aboutsummaryrefslogtreecommitdiffstats
path: root/subprojects
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <marussy@mit.bme.hu>2024-07-21 01:18:00 +0200
committerLibravatar Kristóf Marussy <marussy@mit.bme.hu>2024-07-26 16:36:07 +0200
commit8918b3d766f8ef24a0d8d29c8f55f4dc6a482c2f (patch)
tree8663c4f817326d65749944cbf4053d25f4ef55e4 /subprojects
parentchore(deps): bump dependencies (diff)
downloadrefinery-8918b3d766f8ef24a0d8d29c8f55f4dc6a482c2f.tar.gz
refinery-8918b3d766f8ef24a0d8d29c8f55f4dc6a482c2f.tar.zst
refinery-8918b3d766f8ef24a0d8d29c8f55f4dc6a482c2f.zip
test: ModelSemantics test framework
Diffstat (limited to 'subprojects')
-rw-r--r--subprojects/generator/build.gradle.kts2
-rw-r--r--subprojects/generator/src/main/java/tools/refinery/generator/ModelSemanticsFactory.java15
-rw-r--r--subprojects/generator/src/test/java/tools/refinery/generator/FileBasedSemanticsTest.java32
-rw-r--r--subprojects/generator/src/test/resources/tools/refinery/generator/abstractTypeHierarchy.problem47
-rw-r--r--subprojects/generator/src/test/resources/tools/refinery/generator/typeHierarchy.problem24
-rw-r--r--subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/DynamicTestLoader.java143
-rw-r--r--subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/SemanticsExpectation.java117
-rw-r--r--subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/SemanticsTest.java18
-rw-r--r--subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/SemanticsTestBuilder.java93
-rw-r--r--subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/SemanticsTestCase.java99
-rw-r--r--subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/SemanticsTestCaseBuilder.java95
-rw-r--r--subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/SemanticsTestLoader.java71
-rw-r--r--subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/internal/ChunkAcceptor.java12
-rw-r--r--subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/internal/ChunkHeader.java9
-rw-r--r--subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/internal/CommonHeader.java18
-rw-r--r--subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/internal/ExpectationHeader.java12
-rw-r--r--subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/internal/ProblemSplitter.java92
-rw-r--r--subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/internal/TestCaseHeader.java9
-rw-r--r--subprojects/logic/src/main/java/tools/refinery/logic/term/truthvalue/TruthValue.java1
19 files changed, 907 insertions, 2 deletions
diff --git a/subprojects/generator/build.gradle.kts b/subprojects/generator/build.gradle.kts
index e5cf1bc4..7fa4b849 100644
--- a/subprojects/generator/build.gradle.kts
+++ b/subprojects/generator/build.gradle.kts
@@ -6,6 +6,7 @@
6 6
7plugins { 7plugins {
8 id("tools.refinery.gradle.java-library") 8 id("tools.refinery.gradle.java-library")
9 id("tools.refinery.gradle.java-test-fixtures")
9} 10}
10 11
11mavenArtifact { 12mavenArtifact {
@@ -15,5 +16,6 @@ mavenArtifact {
15dependencies { 16dependencies {
16 api(project(":refinery-language-semantics")) 17 api(project(":refinery-language-semantics"))
17 implementation(project(":refinery-store-query-interpreter")) 18 implementation(project(":refinery-store-query-interpreter"))
19 testFixturesImplementation(libs.junit.api)
18 testImplementation(testFixtures(project(":refinery-language"))) 20 testImplementation(testFixtures(project(":refinery-language")))
19} 21}
diff --git a/subprojects/generator/src/main/java/tools/refinery/generator/ModelSemanticsFactory.java b/subprojects/generator/src/main/java/tools/refinery/generator/ModelSemanticsFactory.java
index 43427502..b624a507 100644
--- a/subprojects/generator/src/main/java/tools/refinery/generator/ModelSemanticsFactory.java
+++ b/subprojects/generator/src/main/java/tools/refinery/generator/ModelSemanticsFactory.java
@@ -12,14 +12,22 @@ import tools.refinery.store.query.interpreter.QueryInterpreterAdapter;
12import tools.refinery.store.reasoning.ReasoningAdapter; 12import tools.refinery.store.reasoning.ReasoningAdapter;
13import tools.refinery.store.reasoning.literal.Concreteness; 13import tools.refinery.store.reasoning.literal.Concreteness;
14 14
15import java.util.Collection;
15import java.util.Set; 16import java.util.Set;
16 17
17public final class ModelSemanticsFactory extends ModelFacadeFactory<ModelSemanticsFactory> { 18public final class ModelSemanticsFactory extends ModelFacadeFactory<ModelSemanticsFactory> {
19 private boolean withCandidateInterpretations;
20
18 @Override 21 @Override
19 protected ModelSemanticsFactory getSelf() { 22 protected ModelSemanticsFactory getSelf() {
20 return this; 23 return this;
21 } 24 }
22 25
26 public ModelSemanticsFactory withCandidateInterpretations(boolean withCandidateInterpretations) {
27 this.withCandidateInterpretations = withCandidateInterpretations;
28 return this;
29 }
30
23 public ModelSemantics createSemantics(Problem problem) { 31 public ModelSemantics createSemantics(Problem problem) {
24 var semantics = tryCreateSemantics(problem); 32 var semantics = tryCreateSemantics(problem);
25 semantics.getPropagationResult().throwIfRejected(); 33 semantics.getPropagationResult().throwIfRejected();
@@ -36,9 +44,14 @@ public final class ModelSemanticsFactory extends ModelFacadeFactory<ModelSemanti
36 .with(PropagationAdapter.builder() 44 .with(PropagationAdapter.builder()
37 .throwOnFatalRejection(false)) 45 .throwOnFatalRejection(false))
38 .with(ReasoningAdapter.builder() 46 .with(ReasoningAdapter.builder()
39 .requiredInterpretations(Set.of(Concreteness.PARTIAL))); 47 .requiredInterpretations(getRequiredInterpretations()));
40 initializer.configureStoreBuilder(storeBuilder, isKeepNonExistingObjects()); 48 initializer.configureStoreBuilder(storeBuilder, isKeepNonExistingObjects());
41 var store = storeBuilder.build(); 49 var store = storeBuilder.build();
42 return new ModelSemantics(initializer.getProblemTrace(), store, initializer.getModelSeed()); 50 return new ModelSemantics(initializer.getProblemTrace(), store, initializer.getModelSeed());
43 } 51 }
52
53 private Collection<Concreteness> getRequiredInterpretations() {
54 return withCandidateInterpretations ? Set.of(Concreteness.PARTIAL, Concreteness.CANDIDATE) :
55 Set.of(Concreteness.PARTIAL);
56 }
44} 57}
diff --git a/subprojects/generator/src/test/java/tools/refinery/generator/FileBasedSemanticsTest.java b/subprojects/generator/src/test/java/tools/refinery/generator/FileBasedSemanticsTest.java
new file mode 100644
index 00000000..6fdcf3fd
--- /dev/null
+++ b/subprojects/generator/src/test/java/tools/refinery/generator/FileBasedSemanticsTest.java
@@ -0,0 +1,32 @@
1/*
2 * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.generator;
7
8import com.google.inject.Inject;
9import org.eclipse.xtext.testing.InjectWith;
10import org.eclipse.xtext.testing.extensions.InjectionExtension;
11import org.junit.jupiter.api.DynamicNode;
12import org.junit.jupiter.api.TestFactory;
13import org.junit.jupiter.api.extension.ExtendWith;
14import tools.refinery.generator.tests.DynamicTestLoader;
15import tools.refinery.language.tests.ProblemInjectorProvider;
16
17import java.util.stream.Stream;
18
19@ExtendWith(InjectionExtension.class)
20@InjectWith(ProblemInjectorProvider.class)
21class FileBasedSemanticsTest {
22 @Inject
23 private DynamicTestLoader loader;
24
25 @TestFactory
26 Stream<DynamicNode> fileBasedTests() {
27 return loader.fromClasspath(getClass(),
28 "abstractTypeHierarchy.problem",
29 "typeHierarchy.problem"
30 );
31 }
32}
diff --git a/subprojects/generator/src/test/resources/tools/refinery/generator/abstractTypeHierarchy.problem b/subprojects/generator/src/test/resources/tools/refinery/generator/abstractTypeHierarchy.problem
new file mode 100644
index 00000000..559c077d
--- /dev/null
+++ b/subprojects/generator/src/test/resources/tools/refinery/generator/abstractTypeHierarchy.problem
@@ -0,0 +1,47 @@
1% SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/>
2%
3% SPDX-License-Identifier: EPL-2.0
4
5abstract class A.
6
7class B extends A.
8
9class C extends A.
10
11class D extends A.
12
13% TEST: negative subclasses
14!B(a).
15!C(a).
16!D(a).
17% EXPECT:
18!A(a).
19
20% TEST: infer subclass by exclusion
21A(a).
22!B(a).
23!C(a).
24% EXPECT:
25D(a).
26
27% TEST: candidate rounding
28A(a).
29% EXPECT EXACTLY:
30?B(a).
31?C(a).
32?D(a).
33% EXPECT CANDIDATE:
34B(a).
35!C(a).
36!D(a).
37
38% TEST: candidate rounding with exclusion
39A(a).
40!B(a).
41% EXPECT EXACTLY:
42?C(a).
43?D(a).
44% EXPECT CANDIDATE:
45!B(a).
46C(a).
47!D(a).
diff --git a/subprojects/generator/src/test/resources/tools/refinery/generator/typeHierarchy.problem b/subprojects/generator/src/test/resources/tools/refinery/generator/typeHierarchy.problem
new file mode 100644
index 00000000..2d297067
--- /dev/null
+++ b/subprojects/generator/src/test/resources/tools/refinery/generator/typeHierarchy.problem
@@ -0,0 +1,24 @@
1% SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/>
2%
3% SPDX-License-Identifier: EPL-2.0
4
5class A.
6
7class B extends A.
8
9% TEST: positive subclass
10B(b).
11% EXPECT:
12A(b).
13
14% TEST: negative superclass
15!A(b).
16% EXPECT:
17!B(b).
18
19% TEST WITH ERRORS: inconsistent type
20!A(b).
21B(b).
22% EXPECT:
23A(b): error.
24B(b): error.
diff --git a/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/DynamicTestLoader.java b/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/DynamicTestLoader.java
new file mode 100644
index 00000000..4a93e77a
--- /dev/null
+++ b/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/DynamicTestLoader.java
@@ -0,0 +1,143 @@
1/*
2 * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.generator.tests;
7
8import com.google.inject.Inject;
9import com.google.inject.Provider;
10import org.junit.jupiter.api.DynamicNode;
11import org.junit.jupiter.api.function.Executable;
12import tools.refinery.generator.ModelSemanticsFactory;
13
14import java.io.IOException;
15import java.net.URI;
16import java.net.URISyntaxException;
17import java.net.URL;
18import java.util.ArrayList;
19import java.util.stream.Stream;
20
21import static org.junit.jupiter.api.DynamicContainer.dynamicContainer;
22import static org.junit.jupiter.api.DynamicTest.dynamicTest;
23
24public class DynamicTestLoader {
25 @Inject
26 private SemanticsTestLoader testLoader;
27
28 @Inject
29 private Provider<ModelSemanticsFactory> semanticsFactoryProvider;
30
31 public Stream<DynamicNode> fromClasspath(Class<?> klass, String name) {
32 URL url;
33 URI uri;
34 try {
35 url = safeGetResource(klass, name);
36 uri = url.toURI();
37 } catch (RuntimeException | URISyntaxException e) {
38 return Stream.of(createErrorNode(name, null, e));
39 }
40 SemanticsTest test;
41 try {
42 test = loadFromUrl(uri, url);
43 } catch (IOException | RuntimeException e) {
44 return Stream.of(createErrorNode(name, uri, e));
45 }
46 return createDynamicNodes(uri, test);
47 }
48
49 public Stream<DynamicNode> fromClasspath(Class<?> klass, String... names) {
50 return fromClasspath(klass, Stream.of(names));
51 }
52
53 public Stream<DynamicNode> fromClasspath(Class<?> klass, Stream<String> names) {
54 return names.map(name -> nodeFromClasspath(klass, name));
55 }
56
57 private DynamicNode nodeFromClasspath(Class<?> klass, String name) {
58 URL url;
59 URI uri;
60 try {
61 url = safeGetResource(klass, name);
62 uri = url.toURI();
63 } catch (RuntimeException | URISyntaxException e) {
64 return createErrorNode(name, null, e);
65 }
66 SemanticsTest test;
67 try {
68 test = loadFromUrl(uri, url);
69 } catch (IOException | RuntimeException e) {
70 return createErrorNode(name, uri, e);
71 }
72 return createDynamicNode(name, uri, test);
73 }
74
75 private static URL safeGetResource(Class<?> klass, String name) {
76 var url = klass.getResource(name);
77 if (url == null) {
78 throw new IllegalStateException("Test resource %s was not found.".formatted(name));
79 }
80 return url;
81 }
82
83 private SemanticsTest loadFromUrl(URI uri, URL url) throws IOException {
84 var eclipseUri = org.eclipse.emf.common.util.URI.createURI(uri.toString());
85 try (var inputStream = url.openStream()) {
86 return testLoader.loadStream(inputStream, eclipseUri);
87 }
88 }
89
90 public Stream<DynamicNode> fromString(String problem) {
91 SemanticsTest test;
92 try {
93 test = testLoader.loadString(problem);
94 } catch (RuntimeException e) {
95 return Stream.of(createErrorNode("<string>", null, e));
96 }
97 return createDynamicNodes(null, test);
98 }
99
100 private DynamicNode createDynamicNode(String name, URI uri, SemanticsTest test) {
101 var testCases = test.testCases();
102 if (testCases.size() == 1 && testCases.getFirst().name() == null) {
103 var testCase = testCases.getFirst();
104 return dynamicTest(name, uri, asExecutable(testCase));
105 }
106 var children = createDynamicNodes(uri, test);
107 return dynamicContainer(name, uri, children);
108 }
109
110 private Stream<DynamicNode> createDynamicNodes(URI uri, SemanticsTest test) {
111 var testCases = test.testCases();
112 var children = new ArrayList<DynamicNode>();
113 int testCaseCount = testCases.size();
114 for (int i = 0; i < testCaseCount; i++) {
115 var testCase = testCases.get(i);
116 var testCaseName = testCase.name();
117 if (testCaseName == null) {
118 testCaseName = "[%d]".formatted(i);
119 }
120 children.add(dynamicTest(testCaseName, uri, asExecutable(testCase)));
121 }
122 return children.stream();
123 }
124
125 private Executable asExecutable(SemanticsTestCase testCase) {
126 return () -> testCase.execute(semanticsFactoryProvider.get());
127 }
128
129 private DynamicNode createErrorNode(String name, URI uri, Throwable cause) {
130 var messageBuilder = new StringBuilder();
131 messageBuilder.append("Error while reading test '").append(name).append("'");
132 if (uri != null) {
133 messageBuilder.append(" from ").append(uri);
134 }
135 if (cause != null) {
136 messageBuilder.append(": ").append(cause.getMessage());
137 }
138 var message = messageBuilder.toString();
139 return dynamicTest(name, uri, () -> {
140 throw new RuntimeException(message, cause);
141 });
142 }
143}
diff --git a/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/SemanticsExpectation.java b/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/SemanticsExpectation.java
new file mode 100644
index 00000000..5cc28cd9
--- /dev/null
+++ b/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/SemanticsExpectation.java
@@ -0,0 +1,117 @@
1/*
2 * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.generator.tests;
7
8import org.eclipse.core.runtime.AssertionFailedException;
9import tools.refinery.generator.FilteredInterpretation;
10import tools.refinery.generator.ModelSemantics;
11import tools.refinery.language.model.problem.Assertion;
12import tools.refinery.language.model.problem.Node;
13import tools.refinery.language.model.problem.NodeAssertionArgument;
14import tools.refinery.language.model.problem.WildcardAssertionArgument;
15import tools.refinery.language.semantics.SemanticsUtils;
16import tools.refinery.logic.term.truthvalue.TruthValue;
17import tools.refinery.store.reasoning.ReasoningAdapter;
18import tools.refinery.store.reasoning.interpretation.PartialInterpretation;
19import tools.refinery.store.reasoning.literal.Concreteness;
20import tools.refinery.store.tuple.Tuple;
21
22public record SemanticsExpectation(Assertion assertion, Concreteness concreteness, boolean exact,
23 int lineNumber, String description, String source) {
24 public void execute(ModelSemantics semantics) {
25 var trace = semantics.getProblemTrace();
26 var symbol = trace.getPartialRelation(assertion.getRelation());
27 var reasoningAdapter = semantics.getModel().getAdapter(ReasoningAdapter.class);
28 var interpretation = reasoningAdapter.getPartialInterpretation(concreteness, symbol);
29 var existsInterpretation = reasoningAdapter.getPartialInterpretation(concreteness,
30 ReasoningAdapter.EXISTS_SYMBOL);
31 var filteredInterpretation = new FilteredInterpretation<>(interpretation, existsInterpretation);
32
33 var arguments = assertion.getArguments();
34 int arity = arguments.size();
35 var nodeIds = new int[arity];
36 boolean wildcard = false;
37 for (int i = 0; i < arity; i++) {
38 var argument = arguments.get(i);
39 switch (argument) {
40 case NodeAssertionArgument nodeAssertionArgument -> {
41 var nodeOrVariable = nodeAssertionArgument.getNode();
42 if (!(nodeOrVariable instanceof Node node)) {
43 throw new IllegalArgumentException("Invalid Node: " + nodeOrVariable);
44 }
45 nodeIds[i] = trace.getNodeId(node);
46 }
47 case WildcardAssertionArgument ignored -> {
48 nodeIds[i] = 1;
49 wildcard = true;
50 }
51 default -> throw new IllegalArgumentException("Invalid AssertionArgument: " + argument);
52 }
53 }
54
55 var expectedValue = SemanticsUtils.getTruthValue(assertion.getValue());
56 if (wildcard) {
57 checkWildcard(filteredInterpretation, nodeIds, expectedValue);
58 } else {
59 checkSingle(filteredInterpretation, nodeIds, expectedValue);
60 }
61 }
62
63
64 private void checkSingle(PartialInterpretation<TruthValue, Boolean> interpretation, int[] nodeIds,
65 TruthValue expectedValue) {
66 var tuple = Tuple.of(nodeIds);
67 var actual = interpretation.get(tuple);
68 if (assertionFailed(expectedValue, actual)) {
69 throw new AssertionFailedException(getMessage(actual));
70 }
71 }
72
73 private void checkWildcard(PartialInterpretation<TruthValue, Boolean> interpretation, int[] nodeIds,
74 TruthValue expectedValue) {
75 int arity = nodeIds.length;
76 var cursor = interpretation.getAll();
77 while (cursor.move()) {
78 var key = cursor.getKey();
79 boolean matches = true;
80 for (int i = 0; matches && i < arity; i++) {
81 int nodeId = nodeIds[i];
82 if (nodeId >= 0 && key.get(i) != nodeId) {
83 matches = false;
84 }
85 }
86 if (matches && assertionFailed(expectedValue, cursor.getValue())) {
87 throw new AssertionFailedException(getMessage(null));
88 }
89 }
90 }
91
92 private boolean assertionFailed(TruthValue expectedValue, TruthValue actual) {
93 return !(exact ? actual.equals(expectedValue) : actual.isRefinementOf(expectedValue));
94 }
95
96 private String getMessage(TruthValue actual) {
97 var messageBuilder = new StringBuilder();
98 messageBuilder.append("EXPECT");
99 if (concreteness == Concreteness.CANDIDATE) {
100 messageBuilder.append(" CANDIDATE");
101 }
102 if (exact) {
103 messageBuilder.append(" EXACTLY");
104 }
105 messageBuilder.append(" ").append(source);
106 if (description != null) {
107 messageBuilder.append(" (").append(description).append(")");
108 }
109 if (actual == null) {
110 messageBuilder.append(" failed");
111 } else {
112 messageBuilder.append(" was ").append(actual).append(" instead");
113 }
114 messageBuilder.append(" in line ").append(lineNumber);
115 return messageBuilder.toString();
116 }
117}
diff --git a/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/SemanticsTest.java b/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/SemanticsTest.java
new file mode 100644
index 00000000..6a34f6ef
--- /dev/null
+++ b/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/SemanticsTest.java
@@ -0,0 +1,18 @@
1/*
2 * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.generator.tests;
7
8import tools.refinery.generator.ModelSemanticsFactory;
9
10import java.util.List;
11
12public record SemanticsTest(List<SemanticsTestCase> testCases) {
13 public void execute(ModelSemanticsFactory semanticsFactory) {
14 for (var testCase : testCases) {
15 testCase.execute(semanticsFactory);
16 }
17 }
18}
diff --git a/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/SemanticsTestBuilder.java b/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/SemanticsTestBuilder.java
new file mode 100644
index 00000000..2d40af5f
--- /dev/null
+++ b/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/SemanticsTestBuilder.java
@@ -0,0 +1,93 @@
1/*
2 * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.generator.tests;
7
8import org.eclipse.emf.common.util.URI;
9import tools.refinery.generator.ProblemLoader;
10import tools.refinery.generator.tests.internal.*;
11
12import java.util.ArrayList;
13import java.util.Collections;
14import java.util.List;
15import java.util.regex.Pattern;
16
17class SemanticsTestBuilder implements ChunkAcceptor {
18 private final Pattern LINE_PATTERN = Pattern.compile("^.+$", Pattern.MULTILINE);
19
20 private final ProblemLoader problemLoader;
21 private final URI uri;
22 private final StringBuilder commonBuilder = new StringBuilder();
23 private final List<SemanticsTestCase> testCases = new ArrayList<>();
24 private SemanticsTestCaseBuilder testCaseBuilder;
25 private boolean singleTestMode;
26
27 public SemanticsTestBuilder(ProblemLoader problemLoader, URI uri) {
28 this.problemLoader = problemLoader;
29 this.uri = uri;
30 }
31
32 @Override
33 public void acceptChunk(ChunkHeader header, String body) {
34 switch (header) {
35 case CommonHeader ignored -> {
36 if (testCaseBuilder != null) {
37 throw new IllegalStateException("Can't accept common test code after the first test case.");
38 }
39 commonBuilder.append(body);
40 }
41 case TestCaseHeader testCaseHeader -> {
42 if (singleTestMode) {
43 throw new IllegalStateException("Can't accept TEST chunk after an EXPECT chunk.");
44 }
45 acceptTestCase(testCaseHeader, body);
46 appendEmptyLines(body);
47 }
48 case ExpectationHeader expectationHeader -> {
49 if (testCaseBuilder == null) {
50 acceptTestCase(new TestCaseHeader(false, null), null);
51 singleTestMode = true;
52 }
53 testCaseBuilder.acceptExpectation(expectationHeader, body);
54 appendEmptyLines(body);
55 }
56 default -> throw new IllegalArgumentException("Unknown ChunkHeader: " + header);
57 }
58 }
59
60 private void appendEmptyLines(String body) {
61 if (singleTestMode) {
62 return;
63 }
64 var placeholder = LINE_PATTERN.matcher(body).replaceAll("");
65 commonBuilder.append(placeholder);
66 }
67
68 private void acceptTestCase(TestCaseHeader header, String body) {
69 if (testCaseBuilder != null) {
70 testCases.add(testCaseBuilder.build());
71 }
72 var problemStringBuilder = new StringBuilder(commonBuilder);
73 if (body != null) {
74 problemStringBuilder.append(body);
75 }
76 testCaseBuilder = new SemanticsTestCaseBuilder(header, problemStringBuilder, problemLoader, uri);
77 }
78
79 @Override
80 public void acceptEnd() {
81 if (testCaseBuilder == null) {
82 throw new IllegalStateException("Test file contained no TEST or EXPECT chunks.");
83 }
84 testCases.add(testCaseBuilder.build());
85 }
86
87 public SemanticsTest build() {
88 if (testCases.isEmpty()) {
89 throw new IllegalStateException("No test cases.");
90 }
91 return new SemanticsTest(Collections.unmodifiableList(testCases));
92 }
93}
diff --git a/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/SemanticsTestCase.java b/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/SemanticsTestCase.java
new file mode 100644
index 00000000..952924e2
--- /dev/null
+++ b/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/SemanticsTestCase.java
@@ -0,0 +1,99 @@
1/*
2 * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.generator.tests;
7
8import org.eclipse.collections.api.factory.primitive.IntObjectMaps;
9import org.eclipse.collections.api.map.primitive.IntObjectMap;
10import org.eclipse.core.runtime.AssertionFailedException;
11import tools.refinery.generator.ModelSemantics;
12import tools.refinery.generator.ModelSemanticsFactory;
13import tools.refinery.language.model.problem.Problem;
14import tools.refinery.language.semantics.ProblemTrace;
15import tools.refinery.logic.term.truthvalue.TruthValue;
16import tools.refinery.store.map.Cursor;
17import tools.refinery.store.reasoning.literal.Concreteness;
18import tools.refinery.store.reasoning.representation.PartialRelation;
19import tools.refinery.store.tuple.Tuple;
20
21import java.util.List;
22
23public record SemanticsTestCase(String name, boolean allowErrors, Problem problem,
24 List<SemanticsExpectation> expectations) {
25 public void execute(ModelSemanticsFactory semanticsFactory) {
26 semanticsFactory.withCandidateInterpretations(needsCandidateInterpretations());
27 var semantics = semanticsFactory.createSemantics(problem);
28 if (!allowErrors) {
29 checkNoErrors(semantics);
30 }
31 for (var expectation : expectations) {
32 expectation.execute(semantics);
33 }
34 }
35
36 public boolean needsCandidateInterpretations() {
37 for (var expectation : expectations) {
38 if (expectation.concreteness() == Concreteness.CANDIDATE) {
39 return true;
40 }
41 }
42 return false;
43 }
44
45 private void checkNoErrors(ModelSemantics semantics) {
46 boolean hasError = false;
47 var errorsBuilder = new StringBuilder("Errors found in partial model:\n\n");
48 var trace = semantics.getProblemTrace();
49 IntObjectMap<String> nodeNames = null;
50 for (var symbol : trace.getRelationTrace().values()) {
51 var interpretation = semantics.getPartialInterpretation(symbol);
52 var cursor = interpretation.getAll();
53 while (cursor.move()) {
54 if (!cursor.getValue().isError()) {
55 continue;
56 }
57 hasError = true;
58 if (nodeNames == null) {
59 nodeNames = getNodeNames(trace);
60 }
61 appendError(symbol, errorsBuilder, cursor, nodeNames);
62 }
63 }
64 if (hasError) {
65 throw new AssertionFailedException(errorsBuilder.toString());
66 }
67 }
68
69 private IntObjectMap<String> getNodeNames(ProblemTrace trace) {
70 var nodeNames = IntObjectMaps.mutable.<String>empty();
71 trace.getNodeTrace().forEachKeyValue((node, i) -> {
72 var name = node.getName();
73 if (name != null) {
74 nodeNames.put(i, name);
75 }
76 });
77 return nodeNames;
78 }
79
80 private static void appendError(PartialRelation symbol, StringBuilder errorsBuilder,
81 Cursor<Tuple, TruthValue> cursor, IntObjectMap<String> nodeNames) {
82 errorsBuilder.append('\t').append(symbol.name()).append("(");
83 var key = cursor.getKey();
84 int arity = key.getSize();
85 for (int i = 0; i < arity; i++) {
86 if (i > 0) {
87 errorsBuilder.append(", ");
88 }
89 int nodeId = key.get(i);
90 var name = nodeNames.get(nodeId);
91 if (name == null) {
92 errorsBuilder.append("::").append(i);
93 } else {
94 errorsBuilder.append(name);
95 }
96 }
97 errorsBuilder.append("): error.\n");
98 }
99}
diff --git a/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/SemanticsTestCaseBuilder.java b/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/SemanticsTestCaseBuilder.java
new file mode 100644
index 00000000..cd163cb8
--- /dev/null
+++ b/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/SemanticsTestCaseBuilder.java
@@ -0,0 +1,95 @@
1/*
2 * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.generator.tests;
7
8import org.eclipse.emf.common.util.URI;
9import org.eclipse.xtext.nodemodel.INode;
10import org.eclipse.xtext.nodemodel.util.NodeModelUtils;
11import tools.refinery.generator.ProblemLoader;
12import tools.refinery.generator.tests.internal.ExpectationHeader;
13import tools.refinery.generator.tests.internal.TestCaseHeader;
14import tools.refinery.language.model.problem.Assertion;
15import tools.refinery.language.model.problem.Problem;
16import tools.refinery.language.model.problem.Statement;
17
18import java.io.IOException;
19import java.util.ArrayDeque;
20import java.util.ArrayList;
21import java.util.Collections;
22import java.util.Deque;
23
24class SemanticsTestCaseBuilder {
25 private final TestCaseHeader testCaseHeader;
26 private final StringBuilder stringBuilder;
27 private final ProblemLoader problemLoader;
28 private final URI uri;
29 private final Deque<ExpectationHeader> expectationsDeque = new ArrayDeque<>();
30
31 public SemanticsTestCaseBuilder(TestCaseHeader testCaseHeader, StringBuilder stringBuilder,
32 ProblemLoader problemLoader, URI uri) {
33 this.testCaseHeader = testCaseHeader;
34 this.stringBuilder = stringBuilder;
35 this.problemLoader = problemLoader;
36 this.uri = uri;
37 }
38
39 public void acceptExpectation(ExpectationHeader header, String body) {
40 stringBuilder.append(body);
41 expectationsDeque.addLast(header);
42 }
43
44 public SemanticsTestCase build() {
45 Problem problem;
46 try {
47 problem = problemLoader.loadString(stringBuilder.toString(), uri);
48 } catch (IOException e) {
49 throw new IllegalStateException("Failed to parse problem: " + uri, e);
50 }
51 if (expectationsDeque.isEmpty() && testCaseHeader.allowErrors()) {
52 throw new IllegalStateException("Test has no EXPECT chunks.");
53 }
54 var statements = problem.getStatements();
55 int initialStatementCount = 0;
56 ExpectationHeader currentHeader = null;
57 var expectations = new ArrayList<SemanticsExpectation>();
58 for (var statement : statements) {
59 var node = NodeModelUtils.findActualNodeFor(statement);
60 if (node == null) {
61 throw new IllegalStateException("No node for statement: " + statement);
62 }
63 var nextHeader = expectationsDeque.peekFirst();
64 if (nextHeader != null && node.getStartLine() >= nextHeader.startLine()) {
65 currentHeader = nextHeader;
66 expectationsDeque.removeFirst();
67 }
68 if (currentHeader == null) {
69 initialStatementCount++;
70 } else {
71 var expectation = createExpectation(currentHeader, statement, node);
72 expectations.add(expectation);
73 }
74 }
75 int statementCount = statements.size();
76 if (statementCount > initialStatementCount) {
77 statements.subList(initialStatementCount, statementCount).clear();
78 }
79 return new SemanticsTestCase(testCaseHeader.name(), testCaseHeader.allowErrors(), problem,
80 Collections.unmodifiableList(expectations));
81 }
82
83 private static SemanticsExpectation createExpectation(ExpectationHeader header, Statement statement,
84 INode node) {
85 if (!(statement instanceof Assertion assertion)) {
86 throw new IllegalArgumentException("Only assertions are supported in EXPECT chunks, got %s instead."
87 .formatted(statement.eClass().getName()));
88 }
89 if (assertion.isDefault()) {
90 throw new IllegalArgumentException("Default assertions are not supported in EXPECT chunks.");
91 }
92 return new SemanticsExpectation(assertion, header.concreteness(), header.exact(),
93 node.getStartLine(), header.description(), NodeModelUtils.getTokenText(node).strip());
94 }
95}
diff --git a/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/SemanticsTestLoader.java b/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/SemanticsTestLoader.java
new file mode 100644
index 00000000..82c8deb1
--- /dev/null
+++ b/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/SemanticsTestLoader.java
@@ -0,0 +1,71 @@
1/*
2 * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.generator.tests;
7
8import com.google.inject.Inject;
9import org.eclipse.emf.common.util.URI;
10import tools.refinery.generator.ProblemLoader;
11import tools.refinery.generator.tests.internal.ProblemSplitter;
12
13import java.io.*;
14import java.nio.charset.StandardCharsets;
15import java.nio.file.Path;
16
17public class SemanticsTestLoader {
18 @Inject
19 private ProblemSplitter problemSplitter;
20
21 @Inject
22 private ProblemLoader problemLoader;
23
24 public SemanticsTestLoader extraPath(String path) {
25 problemLoader.extraPath(Path.of(path));
26 return this;
27 }
28
29 public SemanticsTestLoader extraPath(Path path) {
30 problemLoader.extraPath(path);
31 return this;
32 }
33
34 public SemanticsTest loadString(String problemString, URI uri) {
35 var builder = new SemanticsTestBuilder(problemLoader, uri);
36 problemSplitter.transformProblem(problemString, builder);
37 return builder.build();
38 }
39
40 public SemanticsTest loadString(String problemString) {
41 return loadString(problemString, null);
42 }
43
44 public SemanticsTest loadStream(InputStream inputStream, URI uri) throws IOException {
45 byte[] bytes;
46 try (var outputStream = new ByteArrayOutputStream()) {
47 inputStream.transferTo(outputStream);
48 bytes = outputStream.toByteArray();
49 }
50 var problemString = new String(bytes, StandardCharsets.UTF_8);
51 return loadString(problemString, uri);
52 }
53
54 public SemanticsTest loadStream(InputStream inputStream) throws IOException {
55 return loadStream(inputStream, null);
56 }
57
58 public SemanticsTest loadFile(File file) throws IOException {
59 var uri = URI.createFileURI(file.getAbsolutePath());
60 try (var inputStream = new FileInputStream(file)) {
61 return loadStream(inputStream, uri);
62 }
63 }
64
65 public SemanticsTest loadFile(String filePath) throws IOException {
66 var uri = URI.createFileURI(filePath);
67 try (var inputStream = new FileInputStream(filePath)) {
68 return loadStream(inputStream, uri);
69 }
70 }
71}
diff --git a/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/internal/ChunkAcceptor.java b/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/internal/ChunkAcceptor.java
new file mode 100644
index 00000000..cf867b19
--- /dev/null
+++ b/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/internal/ChunkAcceptor.java
@@ -0,0 +1,12 @@
1/*
2 * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.generator.tests.internal;
7
8public interface ChunkAcceptor {
9 void acceptChunk(ChunkHeader header, String body);
10
11 void acceptEnd();
12}
diff --git a/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/internal/ChunkHeader.java b/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/internal/ChunkHeader.java
new file mode 100644
index 00000000..a69668ec
--- /dev/null
+++ b/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/internal/ChunkHeader.java
@@ -0,0 +1,9 @@
1/*
2 * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.generator.tests.internal;
7
8public sealed interface ChunkHeader permits CommonHeader, TestCaseHeader, ExpectationHeader {
9}
diff --git a/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/internal/CommonHeader.java b/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/internal/CommonHeader.java
new file mode 100644
index 00000000..bec9e748
--- /dev/null
+++ b/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/internal/CommonHeader.java
@@ -0,0 +1,18 @@
1/*
2 * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.generator.tests.internal;
7
8public final class CommonHeader implements ChunkHeader {
9 public static final CommonHeader INSTANCE = new CommonHeader();
10
11 private CommonHeader() {
12 }
13
14 @Override
15 public String toString() {
16 return getClass().getSimpleName() + "[]";
17 }
18}
diff --git a/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/internal/ExpectationHeader.java b/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/internal/ExpectationHeader.java
new file mode 100644
index 00000000..00608739
--- /dev/null
+++ b/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/internal/ExpectationHeader.java
@@ -0,0 +1,12 @@
1/*
2 * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.generator.tests.internal;
7
8import tools.refinery.store.reasoning.literal.Concreteness;
9
10public record ExpectationHeader(Concreteness concreteness, boolean exact, String description,
11 int startLine) implements ChunkHeader {
12}
diff --git a/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/internal/ProblemSplitter.java b/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/internal/ProblemSplitter.java
new file mode 100644
index 00000000..33a0ca6e
--- /dev/null
+++ b/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/internal/ProblemSplitter.java
@@ -0,0 +1,92 @@
1/*
2 * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.generator.tests.internal;
7
8import com.google.inject.Inject;
9import com.google.inject.Injector;
10import com.google.inject.Provider;
11import com.google.inject.Singleton;
12import com.google.inject.name.Named;
13import org.antlr.runtime.ANTLRStringStream;
14import org.antlr.runtime.CommonToken;
15import org.antlr.runtime.Token;
16import org.antlr.runtime.TokenSource;
17import org.eclipse.xtext.parser.antlr.Lexer;
18import tools.refinery.language.parser.antlr.ProblemTokenSource;
19import tools.refinery.language.parser.antlr.lexer.InternalProblemLexer;
20import tools.refinery.store.reasoning.literal.Concreteness;
21
22import java.util.regex.Pattern;
23
24@Singleton
25public class ProblemSplitter {
26 private static final String COMMENT_PREFIX = "(//|%)\\s*";
27
28 private static final Pattern TEST_CASE_PATTERN = Pattern.compile(COMMENT_PREFIX +
29 "TEST(?<allowErrors>\\s+WITH\\s+ERRORS)?(\\s*:\\s*(?<name>\\S.*)?)?");
30
31 private static final Pattern EXPECTATION_PATTERN = Pattern.compile(COMMENT_PREFIX +
32 "EXPECT(?<candidate>\\s+CANDIDATE)?(?<exact>\\s+EXACTLY)?(\\s*:\\s*(?<description>\\S.*)?)?");
33
34 @Inject
35 @Named("org.eclipse.xtext.parser.antlr.Lexer.RUNTIME")
36 private Provider<Lexer> lexerProvider;
37
38 @Inject
39 private Injector injector;
40
41 public void transformProblem(String problemString, ChunkAcceptor acceptor) {
42 var tokenSource = getTokenSource(problemString);
43 Token token = tokenSource.nextToken();
44 ChunkHeader lastHeader = CommonHeader.INSTANCE;
45 int lastStartIndex = 0;
46 do {
47 if (token.getType() == InternalProblemLexer.RULE_SL_COMMENT) {
48 if (!(token instanceof CommonToken commonToken)) {
49 throw new IllegalStateException("Unexpected token: " + token);
50 }
51 var header = parseHeader(token);
52 if (header != null) {
53 int startIndex = commonToken.getStartIndex();
54 var body = problemString.substring(lastStartIndex, startIndex);
55 acceptor.acceptChunk(lastHeader, body);
56 lastHeader = header;
57 lastStartIndex = startIndex;
58 }
59 }
60 token = tokenSource.nextToken();
61 } while (token != null && token.getType() != Token.EOF);
62 acceptor.acceptChunk(lastHeader, problemString.substring(lastStartIndex));
63 acceptor.acceptEnd();
64 }
65
66 private TokenSource getTokenSource(String problemString) {
67 var charStream = new ANTLRStringStream(problemString);
68 var lexer = lexerProvider.get();
69 lexer.setCharStream(charStream);
70 var tokenSource = new ProblemTokenSource(lexer);
71 injector.injectMembers(tokenSource);
72 return tokenSource;
73 }
74
75 private ChunkHeader parseHeader(Token token) {
76 var headerText = token.getText().strip();
77 var testCaseMatcher = TEST_CASE_PATTERN.matcher(headerText);
78 if (testCaseMatcher.matches()) {
79 boolean allowErrors = testCaseMatcher.group("allowErrors") != null;
80 return new TestCaseHeader(allowErrors, testCaseMatcher.group("name"));
81 }
82 var expectationMatcher = EXPECTATION_PATTERN.matcher(headerText);
83 if (expectationMatcher.matches()) {
84 var concreteness = expectationMatcher.group("candidate") == null ? Concreteness.PARTIAL :
85 Concreteness.CANDIDATE;
86 var exact = expectationMatcher.group("exact") != null;
87 return new ExpectationHeader(concreteness, exact, expectationMatcher.group("description"),
88 token.getLine());
89 }
90 return null;
91 }
92}
diff --git a/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/internal/TestCaseHeader.java b/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/internal/TestCaseHeader.java
new file mode 100644
index 00000000..505b4769
--- /dev/null
+++ b/subprojects/generator/src/testFixtures/java/tools/refinery/generator/tests/internal/TestCaseHeader.java
@@ -0,0 +1,9 @@
1/*
2 * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.generator.tests.internal;
7
8public record TestCaseHeader(boolean allowErrors, String name) implements ChunkHeader {
9}
diff --git a/subprojects/logic/src/main/java/tools/refinery/logic/term/truthvalue/TruthValue.java b/subprojects/logic/src/main/java/tools/refinery/logic/term/truthvalue/TruthValue.java
index 59bdeab3..671e1a05 100644
--- a/subprojects/logic/src/main/java/tools/refinery/logic/term/truthvalue/TruthValue.java
+++ b/subprojects/logic/src/main/java/tools/refinery/logic/term/truthvalue/TruthValue.java
@@ -50,7 +50,6 @@ public enum TruthValue implements AbstractValue<TruthValue, Boolean> {
50 return !isError(); 50 return !isError();
51 } 51 }
52 52
53
54 public boolean isComplete() { 53 public boolean isComplete() {
55 return this != UNKNOWN; 54 return this != UNKNOWN;
56 } 55 }