diff options
author | Kristóf Marussy <marussy@mit.bme.hu> | 2024-07-21 01:18:00 +0200 |
---|---|---|
committer | Kristóf Marussy <marussy@mit.bme.hu> | 2024-07-26 16:36:07 +0200 |
commit | 8918b3d766f8ef24a0d8d29c8f55f4dc6a482c2f (patch) | |
tree | 8663c4f817326d65749944cbf4053d25f4ef55e4 /subprojects/generator | |
parent | chore(deps): bump dependencies (diff) | |
download | refinery-8918b3d766f8ef24a0d8d29c8f55f4dc6a482c2f.tar.gz refinery-8918b3d766f8ef24a0d8d29c8f55f4dc6a482c2f.tar.zst refinery-8918b3d766f8ef24a0d8d29c8f55f4dc6a482c2f.zip |
test: ModelSemantics test framework
Diffstat (limited to 'subprojects/generator')
18 files changed, 907 insertions, 1 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 | ||
7 | plugins { | 7 | plugins { |
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 | ||
11 | mavenArtifact { | 12 | mavenArtifact { |
@@ -15,5 +16,6 @@ mavenArtifact { | |||
15 | dependencies { | 16 | dependencies { |
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; | |||
12 | import tools.refinery.store.reasoning.ReasoningAdapter; | 12 | import tools.refinery.store.reasoning.ReasoningAdapter; |
13 | import tools.refinery.store.reasoning.literal.Concreteness; | 13 | import tools.refinery.store.reasoning.literal.Concreteness; |
14 | 14 | ||
15 | import java.util.Collection; | ||
15 | import java.util.Set; | 16 | import java.util.Set; |
16 | 17 | ||
17 | public final class ModelSemanticsFactory extends ModelFacadeFactory<ModelSemanticsFactory> { | 18 | public 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 | */ | ||
6 | package tools.refinery.generator; | ||
7 | |||
8 | import com.google.inject.Inject; | ||
9 | import org.eclipse.xtext.testing.InjectWith; | ||
10 | import org.eclipse.xtext.testing.extensions.InjectionExtension; | ||
11 | import org.junit.jupiter.api.DynamicNode; | ||
12 | import org.junit.jupiter.api.TestFactory; | ||
13 | import org.junit.jupiter.api.extension.ExtendWith; | ||
14 | import tools.refinery.generator.tests.DynamicTestLoader; | ||
15 | import tools.refinery.language.tests.ProblemInjectorProvider; | ||
16 | |||
17 | import java.util.stream.Stream; | ||
18 | |||
19 | @ExtendWith(InjectionExtension.class) | ||
20 | @InjectWith(ProblemInjectorProvider.class) | ||
21 | class 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 | |||
5 | abstract class A. | ||
6 | |||
7 | class B extends A. | ||
8 | |||
9 | class C extends A. | ||
10 | |||
11 | class 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 | ||
21 | A(a). | ||
22 | !B(a). | ||
23 | !C(a). | ||
24 | % EXPECT: | ||
25 | D(a). | ||
26 | |||
27 | % TEST: candidate rounding | ||
28 | A(a). | ||
29 | % EXPECT EXACTLY: | ||
30 | ?B(a). | ||
31 | ?C(a). | ||
32 | ?D(a). | ||
33 | % EXPECT CANDIDATE: | ||
34 | B(a). | ||
35 | !C(a). | ||
36 | !D(a). | ||
37 | |||
38 | % TEST: candidate rounding with exclusion | ||
39 | A(a). | ||
40 | !B(a). | ||
41 | % EXPECT EXACTLY: | ||
42 | ?C(a). | ||
43 | ?D(a). | ||
44 | % EXPECT CANDIDATE: | ||
45 | !B(a). | ||
46 | C(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 | |||
5 | class A. | ||
6 | |||
7 | class B extends A. | ||
8 | |||
9 | % TEST: positive subclass | ||
10 | B(b). | ||
11 | % EXPECT: | ||
12 | A(b). | ||
13 | |||
14 | % TEST: negative superclass | ||
15 | !A(b). | ||
16 | % EXPECT: | ||
17 | !B(b). | ||
18 | |||
19 | % TEST WITH ERRORS: inconsistent type | ||
20 | !A(b). | ||
21 | B(b). | ||
22 | % EXPECT: | ||
23 | A(b): error. | ||
24 | B(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 | */ | ||
6 | package tools.refinery.generator.tests; | ||
7 | |||
8 | import com.google.inject.Inject; | ||
9 | import com.google.inject.Provider; | ||
10 | import org.junit.jupiter.api.DynamicNode; | ||
11 | import org.junit.jupiter.api.function.Executable; | ||
12 | import tools.refinery.generator.ModelSemanticsFactory; | ||
13 | |||
14 | import java.io.IOException; | ||
15 | import java.net.URI; | ||
16 | import java.net.URISyntaxException; | ||
17 | import java.net.URL; | ||
18 | import java.util.ArrayList; | ||
19 | import java.util.stream.Stream; | ||
20 | |||
21 | import static org.junit.jupiter.api.DynamicContainer.dynamicContainer; | ||
22 | import static org.junit.jupiter.api.DynamicTest.dynamicTest; | ||
23 | |||
24 | public 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 | */ | ||
6 | package tools.refinery.generator.tests; | ||
7 | |||
8 | import org.eclipse.core.runtime.AssertionFailedException; | ||
9 | import tools.refinery.generator.FilteredInterpretation; | ||
10 | import tools.refinery.generator.ModelSemantics; | ||
11 | import tools.refinery.language.model.problem.Assertion; | ||
12 | import tools.refinery.language.model.problem.Node; | ||
13 | import tools.refinery.language.model.problem.NodeAssertionArgument; | ||
14 | import tools.refinery.language.model.problem.WildcardAssertionArgument; | ||
15 | import tools.refinery.language.semantics.SemanticsUtils; | ||
16 | import tools.refinery.logic.term.truthvalue.TruthValue; | ||
17 | import tools.refinery.store.reasoning.ReasoningAdapter; | ||
18 | import tools.refinery.store.reasoning.interpretation.PartialInterpretation; | ||
19 | import tools.refinery.store.reasoning.literal.Concreteness; | ||
20 | import tools.refinery.store.tuple.Tuple; | ||
21 | |||
22 | public 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 | */ | ||
6 | package tools.refinery.generator.tests; | ||
7 | |||
8 | import tools.refinery.generator.ModelSemanticsFactory; | ||
9 | |||
10 | import java.util.List; | ||
11 | |||
12 | public 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 | */ | ||
6 | package tools.refinery.generator.tests; | ||
7 | |||
8 | import org.eclipse.emf.common.util.URI; | ||
9 | import tools.refinery.generator.ProblemLoader; | ||
10 | import tools.refinery.generator.tests.internal.*; | ||
11 | |||
12 | import java.util.ArrayList; | ||
13 | import java.util.Collections; | ||
14 | import java.util.List; | ||
15 | import java.util.regex.Pattern; | ||
16 | |||
17 | class 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 | */ | ||
6 | package tools.refinery.generator.tests; | ||
7 | |||
8 | import org.eclipse.collections.api.factory.primitive.IntObjectMaps; | ||
9 | import org.eclipse.collections.api.map.primitive.IntObjectMap; | ||
10 | import org.eclipse.core.runtime.AssertionFailedException; | ||
11 | import tools.refinery.generator.ModelSemantics; | ||
12 | import tools.refinery.generator.ModelSemanticsFactory; | ||
13 | import tools.refinery.language.model.problem.Problem; | ||
14 | import tools.refinery.language.semantics.ProblemTrace; | ||
15 | import tools.refinery.logic.term.truthvalue.TruthValue; | ||
16 | import tools.refinery.store.map.Cursor; | ||
17 | import tools.refinery.store.reasoning.literal.Concreteness; | ||
18 | import tools.refinery.store.reasoning.representation.PartialRelation; | ||
19 | import tools.refinery.store.tuple.Tuple; | ||
20 | |||
21 | import java.util.List; | ||
22 | |||
23 | public 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 | */ | ||
6 | package tools.refinery.generator.tests; | ||
7 | |||
8 | import org.eclipse.emf.common.util.URI; | ||
9 | import org.eclipse.xtext.nodemodel.INode; | ||
10 | import org.eclipse.xtext.nodemodel.util.NodeModelUtils; | ||
11 | import tools.refinery.generator.ProblemLoader; | ||
12 | import tools.refinery.generator.tests.internal.ExpectationHeader; | ||
13 | import tools.refinery.generator.tests.internal.TestCaseHeader; | ||
14 | import tools.refinery.language.model.problem.Assertion; | ||
15 | import tools.refinery.language.model.problem.Problem; | ||
16 | import tools.refinery.language.model.problem.Statement; | ||
17 | |||
18 | import java.io.IOException; | ||
19 | import java.util.ArrayDeque; | ||
20 | import java.util.ArrayList; | ||
21 | import java.util.Collections; | ||
22 | import java.util.Deque; | ||
23 | |||
24 | class 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 | */ | ||
6 | package tools.refinery.generator.tests; | ||
7 | |||
8 | import com.google.inject.Inject; | ||
9 | import org.eclipse.emf.common.util.URI; | ||
10 | import tools.refinery.generator.ProblemLoader; | ||
11 | import tools.refinery.generator.tests.internal.ProblemSplitter; | ||
12 | |||
13 | import java.io.*; | ||
14 | import java.nio.charset.StandardCharsets; | ||
15 | import java.nio.file.Path; | ||
16 | |||
17 | public 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 | */ | ||
6 | package tools.refinery.generator.tests.internal; | ||
7 | |||
8 | public 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 | */ | ||
6 | package tools.refinery.generator.tests.internal; | ||
7 | |||
8 | public 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 | */ | ||
6 | package tools.refinery.generator.tests.internal; | ||
7 | |||
8 | public 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 | */ | ||
6 | package tools.refinery.generator.tests.internal; | ||
7 | |||
8 | import tools.refinery.store.reasoning.literal.Concreteness; | ||
9 | |||
10 | public 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 | */ | ||
6 | package tools.refinery.generator.tests.internal; | ||
7 | |||
8 | import com.google.inject.Inject; | ||
9 | import com.google.inject.Injector; | ||
10 | import com.google.inject.Provider; | ||
11 | import com.google.inject.Singleton; | ||
12 | import com.google.inject.name.Named; | ||
13 | import org.antlr.runtime.ANTLRStringStream; | ||
14 | import org.antlr.runtime.CommonToken; | ||
15 | import org.antlr.runtime.Token; | ||
16 | import org.antlr.runtime.TokenSource; | ||
17 | import org.eclipse.xtext.parser.antlr.Lexer; | ||
18 | import tools.refinery.language.parser.antlr.ProblemTokenSource; | ||
19 | import tools.refinery.language.parser.antlr.lexer.InternalProblemLexer; | ||
20 | import tools.refinery.store.reasoning.literal.Concreteness; | ||
21 | |||
22 | import java.util.regex.Pattern; | ||
23 | |||
24 | @Singleton | ||
25 | public 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 | */ | ||
6 | package tools.refinery.generator.tests.internal; | ||
7 | |||
8 | public record TestCaseHeader(boolean allowErrors, String name) implements ChunkHeader { | ||
9 | } | ||