aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLibravatar Kristóf Marussy <kristof@marussy.com>2023-11-15 02:19:06 +0100
committerLibravatar Kristóf Marussy <kristof@marussy.com>2023-11-17 12:41:35 +0100
commitac478b828e5c6dbea72df1303cbe1e0da0fde42e (patch)
treea91419a2f2a591ab3cc1b06d750db95c817b550d
parentfix: scope upper bounds (diff)
downloadrefinery-ac478b828e5c6dbea72df1303cbe1e0da0fde42e.tar.gz
refinery-ac478b828e5c6dbea72df1303cbe1e0da0fde42e.tar.zst
refinery-ac478b828e5c6dbea72df1303cbe1e0da0fde42e.zip
feat(language): opposite reference validation
-rw-r--r--subprojects/language-semantics/src/test/java/tools/refinery/language/semantics/ModelGenerationTest.java6
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/Problem.xtext5
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/ProblemRuntimeModule.java6
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/utils/ProblemUtil.java32
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/validation/ProblemDiagnosticConverter.java31
-rw-r--r--subprojects/language/src/main/java/tools/refinery/language/validation/ProblemValidator.java108
-rw-r--r--subprojects/language/src/test/java/tools/refinery/language/tests/ProblemParsingTest.java2
-rw-r--r--subprojects/language/src/test/java/tools/refinery/language/tests/linking/AmbiguousReferenceTest.java10
-rw-r--r--subprojects/language/src/test/java/tools/refinery/language/tests/parser/antlr/TransitiveClosureParserTest.java4
-rw-r--r--subprojects/language/src/test/java/tools/refinery/language/tests/rules/RuleParsingTest.java10
-rw-r--r--subprojects/language/src/test/java/tools/refinery/language/tests/scoping/NodeScopingTest.java28
-rw-r--r--subprojects/language/src/test/java/tools/refinery/language/tests/validation/MultiplicityValidationTest.java119
-rw-r--r--subprojects/language/src/test/java/tools/refinery/language/tests/validation/OppositeValidationTest.java209
-rw-r--r--subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/ProblemParseHelper.java13
-rw-r--r--subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/WrappedProblem.java13
15 files changed, 555 insertions, 41 deletions
diff --git a/subprojects/language-semantics/src/test/java/tools/refinery/language/semantics/ModelGenerationTest.java b/subprojects/language-semantics/src/test/java/tools/refinery/language/semantics/ModelGenerationTest.java
index 899e3cb3..b4abce81 100644
--- a/subprojects/language-semantics/src/test/java/tools/refinery/language/semantics/ModelGenerationTest.java
+++ b/subprojects/language-semantics/src/test/java/tools/refinery/language/semantics/ModelGenerationTest.java
@@ -77,7 +77,7 @@ class ModelGenerationTest {
77 % Scope 77 % Scope
78 scope Post = 5, Person = 5. 78 scope Post = 5, Person = 5.
79 """); 79 """);
80 assertThat(parsedProblem.errors(), empty()); 80 assertThat(parsedProblem.getResourceErrors(), empty());
81 var problem = parsedProblem.problem(); 81 var problem = parsedProblem.problem();
82 82
83 var storeBuilder = ModelStore.builder() 83 var storeBuilder = ModelStore.builder()
@@ -211,7 +211,7 @@ class ModelGenerationTest {
211 211
212 scope node = 200..210, Region = 10..*, Choice = 1..*, Statechart = 1. 212 scope node = 200..210, Region = 10..*, Choice = 1..*, Statechart = 1.
213 """); 213 """);
214 assertThat(parsedProblem.errors(), empty()); 214 assertThat(parsedProblem.getResourceErrors(), empty());
215 var problem = parsedProblem.problem(); 215 var problem = parsedProblem.problem();
216 216
217 var storeBuilder = ModelStore.builder() 217 var storeBuilder = ModelStore.builder()
@@ -278,7 +278,7 @@ class ModelGenerationTest {
278 278
279 scope Filesystem += 0, Entry = 100. 279 scope Filesystem += 0, Entry = 100.
280 """); 280 """);
281 assertThat(parsedProblem.errors(), empty()); 281 assertThat(parsedProblem.getResourceErrors(), empty());
282 var problem = parsedProblem.problem(); 282 var problem = parsedProblem.problem();
283 283
284 var storeBuilder = ModelStore.builder() 284 var storeBuilder = ModelStore.builder()
diff --git a/subprojects/language/src/main/java/tools/refinery/language/Problem.xtext b/subprojects/language/src/main/java/tools/refinery/language/Problem.xtext
index 0a91178b..0fb96954 100644
--- a/subprojects/language/src/main/java/tools/refinery/language/Problem.xtext
+++ b/subprojects/language/src/main/java/tools/refinery/language/Problem.xtext
@@ -40,10 +40,13 @@ enum ReferenceKind:
40ReferenceDeclaration: 40ReferenceDeclaration:
41 (referenceType=[Relation|NonContainmentQualifiedName] | 41 (referenceType=[Relation|NonContainmentQualifiedName] |
42 kind=ReferenceKind referenceType=[Relation|QualifiedName]) 42 kind=ReferenceKind referenceType=[Relation|QualifiedName])
43 ("[" multiplicity=Multiplicity "]")? 43 (multiplicity=ReferenceMultiplicity)?
44 name=Identifier 44 name=Identifier
45 ("opposite" opposite=[ReferenceDeclaration|QualifiedName])?; 45 ("opposite" opposite=[ReferenceDeclaration|QualifiedName])?;
46 46
47ReferenceMultiplicity returns Multiplicity:
48 "[" Multiplicity "]";
49
47//enum PrimitiveType: 50//enum PrimitiveType:
48// INT="int" | REAL="real" | STRING="string"; 51// INT="int" | REAL="real" | STRING="string";
49// 52//
diff --git a/subprojects/language/src/main/java/tools/refinery/language/ProblemRuntimeModule.java b/subprojects/language/src/main/java/tools/refinery/language/ProblemRuntimeModule.java
index 100f3781..0a5cb3c2 100644
--- a/subprojects/language/src/main/java/tools/refinery/language/ProblemRuntimeModule.java
+++ b/subprojects/language/src/main/java/tools/refinery/language/ProblemRuntimeModule.java
@@ -20,6 +20,7 @@ import org.eclipse.xtext.scoping.IGlobalScopeProvider;
20import org.eclipse.xtext.scoping.IScopeProvider; 20import org.eclipse.xtext.scoping.IScopeProvider;
21import org.eclipse.xtext.scoping.impl.AbstractDeclarativeScopeProvider; 21import org.eclipse.xtext.scoping.impl.AbstractDeclarativeScopeProvider;
22import org.eclipse.xtext.serializer.sequencer.ISemanticSequencer; 22import org.eclipse.xtext.serializer.sequencer.ISemanticSequencer;
23import org.eclipse.xtext.validation.IDiagnosticConverter;
23import org.eclipse.xtext.validation.IResourceValidator; 24import org.eclipse.xtext.validation.IResourceValidator;
24import org.eclipse.xtext.xbase.annotations.validation.DerivedStateAwareResourceValidator; 25import org.eclipse.xtext.xbase.annotations.validation.DerivedStateAwareResourceValidator;
25import tools.refinery.language.conversion.ProblemValueConverterService; 26import tools.refinery.language.conversion.ProblemValueConverterService;
@@ -33,6 +34,7 @@ import tools.refinery.language.resource.ProblemResourceDescriptionStrategy;
33import tools.refinery.language.scoping.ProblemGlobalScopeProvider; 34import tools.refinery.language.scoping.ProblemGlobalScopeProvider;
34import tools.refinery.language.scoping.ProblemLocalScopeProvider; 35import tools.refinery.language.scoping.ProblemLocalScopeProvider;
35import tools.refinery.language.serializer.PreferShortAssertionsProblemSemanticSequencer; 36import tools.refinery.language.serializer.PreferShortAssertionsProblemSemanticSequencer;
37import tools.refinery.language.validation.ProblemDiagnosticConverter;
36 38
37/** 39/**
38 * Use this class to register components to be used at runtime / without the 40 * Use this class to register components to be used at runtime / without the
@@ -101,4 +103,8 @@ public class ProblemRuntimeModule extends AbstractProblemRuntimeModule {
101 public Class<? extends ISemanticSequencer> bindISemanticSequencer() { 103 public Class<? extends ISemanticSequencer> bindISemanticSequencer() {
102 return PreferShortAssertionsProblemSemanticSequencer.class; 104 return PreferShortAssertionsProblemSemanticSequencer.class;
103 } 105 }
106
107 public Class<? extends IDiagnosticConverter> bindIDiagnosticConverter() {
108 return ProblemDiagnosticConverter.class;
109 }
104} 110}
diff --git a/subprojects/language/src/main/java/tools/refinery/language/utils/ProblemUtil.java b/subprojects/language/src/main/java/tools/refinery/language/utils/ProblemUtil.java
index b3c93b07..7b6407e1 100644
--- a/subprojects/language/src/main/java/tools/refinery/language/utils/ProblemUtil.java
+++ b/subprojects/language/src/main/java/tools/refinery/language/utils/ProblemUtil.java
@@ -7,11 +7,12 @@ package tools.refinery.language.utils;
7 7
8import org.eclipse.emf.common.util.URI; 8import org.eclipse.emf.common.util.URI;
9import org.eclipse.emf.ecore.EObject; 9import org.eclipse.emf.ecore.EObject;
10import org.eclipse.emf.ecore.util.EcoreUtil;
10import tools.refinery.language.model.problem.*; 11import tools.refinery.language.model.problem.*;
11 12
12public final class ProblemUtil { 13public final class ProblemUtil {
13 public static final String BUILTIN_LIBRARY_NAME = "builtin"; 14 public static final String BUILTIN_LIBRARY_NAME = "builtin";
14 public static final URI BUILTIN_LIBRARY_URI = getLibraryUri(BUILTIN_LIBRARY_NAME); 15 public static final URI BUILTIN_LIBRARY_URI = getLibraryUri();
15 16
16 private ProblemUtil() { 17 private ProblemUtil() {
17 throw new IllegalStateException("This is a static utility class and should not be instantiated directly"); 18 throw new IllegalStateException("This is a static utility class and should not be instantiated directly");
@@ -96,8 +97,31 @@ public final class ProblemUtil {
96 throw new IllegalArgumentException("Unknown Relation: " + relation); 97 throw new IllegalArgumentException("Unknown Relation: " + relation);
97 } 98 }
98 99
99 private static URI getLibraryUri(String libraryName) { 100 public static boolean isContainerReference(ReferenceDeclaration referenceDeclaration) {
100 return URI.createURI(ProblemUtil.class.getClassLoader() 101 var kind = referenceDeclaration.getKind();
101 .getResource("tools/refinery/language/%s.problem".formatted(libraryName)).toString()); 102 if (kind == null) {
103 return false;
104 }
105 return switch (kind) {
106 case CONTAINMENT -> false;
107 case CONTAINER -> true;
108 case REFERENCE -> {
109 var opposite = referenceDeclaration.getOpposite();
110 if (opposite == null) {
111 yield false;
112 }
113 opposite = (ReferenceDeclaration) EcoreUtil.resolve(opposite, referenceDeclaration);
114 yield opposite.getKind() == ReferenceKind.CONTAINMENT;
115 }
116 };
117 }
118
119 private static URI getLibraryUri() {
120 var libraryResource = ProblemUtil.class.getClassLoader()
121 .getResource("tools/refinery/language/%s.problem".formatted(BUILTIN_LIBRARY_NAME));
122 if (libraryResource == null) {
123 throw new AssertionError("Library '%s' was not found".formatted(BUILTIN_LIBRARY_NAME));
124 }
125 return URI.createURI(libraryResource.toString());
102 } 126 }
103} 127}
diff --git a/subprojects/language/src/main/java/tools/refinery/language/validation/ProblemDiagnosticConverter.java b/subprojects/language/src/main/java/tools/refinery/language/validation/ProblemDiagnosticConverter.java
new file mode 100644
index 00000000..0b7cc315
--- /dev/null
+++ b/subprojects/language/src/main/java/tools/refinery/language/validation/ProblemDiagnosticConverter.java
@@ -0,0 +1,31 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.language.validation;
7
8import com.google.inject.Inject;
9import org.eclipse.emf.ecore.EObject;
10import org.eclipse.emf.ecore.EStructuralFeature;
11import org.eclipse.xtext.validation.DiagnosticConverterImpl;
12import tools.refinery.language.model.problem.Multiplicity;
13import tools.refinery.language.model.problem.ReferenceDeclaration;
14import tools.refinery.language.services.ProblemGrammarAccess;
15
16public class ProblemDiagnosticConverter extends DiagnosticConverterImpl {
17 @Inject
18 private ProblemGrammarAccess grammarAccess;
19
20 @Override
21 protected IssueLocation getLocationData(EObject obj, EStructuralFeature structuralFeature, int index) {
22 if (structuralFeature == null && obj instanceof Multiplicity &&
23 obj.eContainer() instanceof ReferenceDeclaration referenceDeclaration) {
24 // Include the enclosing {@code []} square braces in the error location.
25 // This lets use have a non-0 length error marker for invalid container references such as
26 // {@code container Foo[] foo opposite bar}, where unbounded multiplicities are disallowed.
27 return getLocationData(referenceDeclaration, obj.eContainingFeature());
28 }
29 return super.getLocationData(obj, structuralFeature, index);
30 }
31}
diff --git a/subprojects/language/src/main/java/tools/refinery/language/validation/ProblemValidator.java b/subprojects/language/src/main/java/tools/refinery/language/validation/ProblemValidator.java
index ef04726b..21b175ee 100644
--- a/subprojects/language/src/main/java/tools/refinery/language/validation/ProblemValidator.java
+++ b/subprojects/language/src/main/java/tools/refinery/language/validation/ProblemValidator.java
@@ -35,6 +35,14 @@ public class ProblemValidator extends AbstractProblemValidator {
35 35
36 public static final String DUPLICATE_NAME_ISSUE = ISSUE_PREFIX + "DUPLICATE_NAME"; 36 public static final String DUPLICATE_NAME_ISSUE = ISSUE_PREFIX + "DUPLICATE_NAME";
37 37
38 public static final String INVALID_MULTIPLICITY_ISSUE = ISSUE_PREFIX + "INVALID_MULTIPLICITY";
39
40 public static final String ZERO_MULTIPLICITY_ISSUE = ISSUE_PREFIX + "ZERO_MULTIPLICITY";
41
42 public static final String MISSING_OPPOSITE_ISSUE = ISSUE_PREFIX + "MISSING_OPPOSITE";
43
44 public static final String INVALID_OPPOSITE_ISSUE = ISSUE_PREFIX + "INVALID_OPPOSITE";
45
38 @Inject 46 @Inject
39 private ReferenceCounter referenceCounter; 47 private ReferenceCounter referenceCounter;
40 48
@@ -111,4 +119,104 @@ public class ProblemValidator extends AbstractProblemValidator {
111 } 119 }
112 } 120 }
113 } 121 }
122
123 @Check
124 public void checkRangeMultiplicity(RangeMultiplicity rangeMultiplicity) {
125 int lower = rangeMultiplicity.getLowerBound();
126 int upper = rangeMultiplicity.getUpperBound();
127 if (upper >= 0 && lower > upper) {
128 var message = "Multiplicity range [%d..%d] is inconsistent.";
129 acceptError(message, rangeMultiplicity, null, 0, INVALID_MULTIPLICITY_ISSUE);
130 }
131 }
132
133 @Check
134 public void checkReferenceMultiplicity(ReferenceDeclaration referenceDeclaration) {
135 var multiplicity = referenceDeclaration.getMultiplicity();
136 if (multiplicity == null) {
137 return;
138 }
139 if (ProblemUtil.isContainerReference(referenceDeclaration) && (
140 !(multiplicity instanceof RangeMultiplicity rangeMultiplicity) ||
141 rangeMultiplicity.getLowerBound() != 0 ||
142 rangeMultiplicity.getUpperBound() != 1)) {
143 var message = "The only allowed multiplicity for container references is [0..1]";
144 acceptError(message, multiplicity, null, 0, INVALID_MULTIPLICITY_ISSUE);
145 }
146 if ((multiplicity instanceof ExactMultiplicity exactMultiplicity &&
147 exactMultiplicity.getExactValue() == 0) ||
148 (multiplicity instanceof RangeMultiplicity rangeMultiplicity &&
149 rangeMultiplicity.getLowerBound() == 0 &&
150 rangeMultiplicity.getUpperBound() == 0)) {
151 var message = "The multiplicity constraint does not allow any reference links";
152 acceptWarning(message, multiplicity, null, 0, ZERO_MULTIPLICITY_ISSUE);
153 }
154 }
155
156 @Check
157 public void checkOpposite(ReferenceDeclaration referenceDeclaration) {
158 var opposite = referenceDeclaration.getOpposite();
159 if (opposite == null || opposite.eIsProxy()) {
160 return;
161 }
162 var oppositeOfOpposite = opposite.getOpposite();
163 if (oppositeOfOpposite == null) {
164 acceptError("Reference '%s' does not declare '%s' as an opposite."
165 .formatted(opposite.getName(), referenceDeclaration.getName()),
166 referenceDeclaration, ProblemPackage.Literals.REFERENCE_DECLARATION__OPPOSITE, 0,
167 INVALID_OPPOSITE_ISSUE);
168 var oppositeResource = opposite.eResource();
169 if (oppositeResource != null && oppositeResource.equals(referenceDeclaration.eResource())) {
170 acceptError("Missing opposite '%s' for reference '%s'."
171 .formatted(referenceDeclaration.getName(), opposite.getName()),
172 opposite, ProblemPackage.Literals.NAMED_ELEMENT__NAME, 0, MISSING_OPPOSITE_ISSUE);
173 }
174 return;
175 }
176 if (!referenceDeclaration.equals(oppositeOfOpposite)) {
177 var messageBuilder = new StringBuilder()
178 .append("Expected reference '")
179 .append(opposite.getName())
180 .append("' to have opposite '")
181 .append(referenceDeclaration.getName())
182 .append("'");
183 var oppositeOfOppositeName = oppositeOfOpposite.getName();
184 if (oppositeOfOppositeName != null) {
185 messageBuilder.append(", got '")
186 .append(oppositeOfOppositeName)
187 .append("' instead");
188 }
189 messageBuilder.append(".");
190 acceptError(messageBuilder.toString(), referenceDeclaration,
191 ProblemPackage.Literals.REFERENCE_DECLARATION__OPPOSITE, 0, INVALID_OPPOSITE_ISSUE);
192 }
193 }
194
195 @Check
196 void checkContainerOpposite(ReferenceDeclaration referenceDeclaration) {
197 var kind = referenceDeclaration.getKind();
198 var opposite = referenceDeclaration.getOpposite();
199 if (opposite != null && opposite.eIsProxy()) {
200 // If {@code opposite} is a proxy, we have already emitted a linker error.
201 return;
202 }
203 if (kind == ReferenceKind.CONTAINMENT) {
204 if (opposite != null && opposite.getKind() == ReferenceKind.CONTAINMENT) {
205 acceptError("Opposite '%s' of containment reference '%s' is not a container reference."
206 .formatted(opposite.getName(), referenceDeclaration.getName()),
207 referenceDeclaration, ProblemPackage.Literals.REFERENCE_DECLARATION__OPPOSITE, 0,
208 INVALID_OPPOSITE_ISSUE);
209 }
210 } else if (kind == ReferenceKind.CONTAINER) {
211 if (opposite == null) {
212 acceptError("Container reference '%s' requires an opposite.".formatted(referenceDeclaration.getName()),
213 referenceDeclaration, ProblemPackage.Literals.NAMED_ELEMENT__NAME, 0, MISSING_OPPOSITE_ISSUE);
214 } else if (opposite.getKind() != ReferenceKind.CONTAINMENT) {
215 acceptError("Opposite '%s' of container reference '%s' is not a containment reference."
216 .formatted(opposite.getName(), referenceDeclaration.getName()),
217 referenceDeclaration, ProblemPackage.Literals.REFERENCE_DECLARATION__OPPOSITE, 0,
218 INVALID_OPPOSITE_ISSUE);
219 }
220 }
221 }
114} 222}
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/ProblemParsingTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/ProblemParsingTest.java
index 96e7cf9c..72d57f54 100644
--- a/subprojects/language/src/test/java/tools/refinery/language/tests/ProblemParsingTest.java
+++ b/subprojects/language/src/test/java/tools/refinery/language/tests/ProblemParsingTest.java
@@ -51,6 +51,6 @@ class ProblemParsingTest {
51 ?children(bob, ciri). 51 ?children(bob, ciri).
52 taxStatus(anne, ADULT). 52 taxStatus(anne, ADULT).
53 """); 53 """);
54 assertThat(problem.errors(), empty()); 54 assertThat(problem.getResourceErrors(), empty());
55 } 55 }
56} 56}
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/linking/AmbiguousReferenceTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/linking/AmbiguousReferenceTest.java
index b1b24ef3..464c207c 100644
--- a/subprojects/language/src/test/java/tools/refinery/language/tests/linking/AmbiguousReferenceTest.java
+++ b/subprojects/language/src/test/java/tools/refinery/language/tests/linking/AmbiguousReferenceTest.java
@@ -7,6 +7,7 @@ package tools.refinery.language.tests.linking;
7 7
8 8
9import com.google.inject.Inject; 9import com.google.inject.Inject;
10import org.eclipse.xtext.diagnostics.Diagnostic;
10import org.eclipse.xtext.testing.InjectWith; 11import org.eclipse.xtext.testing.InjectWith;
11import org.eclipse.xtext.testing.extensions.InjectionExtension; 12import org.eclipse.xtext.testing.extensions.InjectionExtension;
12import org.junit.jupiter.api.extension.ExtendWith; 13import org.junit.jupiter.api.extension.ExtendWith;
@@ -57,7 +58,7 @@ class AmbiguousReferenceTest {
57 """}) 58 """})
58 void unambiguousReferenceTest(String text) { 59 void unambiguousReferenceTest(String text) {
59 var problem = parseHelper.parse(text); 60 var problem = parseHelper.parse(text);
60 assertThat(problem.errors(), empty()); 61 assertThat(problem.getResourceErrors(), empty());
61 } 62 }
62 63
63 @ParameterizedTest 64 @ParameterizedTest
@@ -88,7 +89,10 @@ class AmbiguousReferenceTest {
88 """}) 89 """})
89 void ambiguousReferenceTest(String text) { 90 void ambiguousReferenceTest(String text) {
90 var problem = parseHelper.parse(text); 91 var problem = parseHelper.parse(text);
91 assertThat(problem.errors(), hasItem(hasProperty("message", stringContainsInOrder( 92 var errors = problem.getResourceErrors();
92 "Ambiguous reference", "'quux'")))); 93 assertThat(problem.getResourceErrors(), hasItem(allOf(
94 hasProperty("code", is(Diagnostic.LINKING_DIAGNOSTIC)),
95 hasProperty("message", containsString("'quux'"))
96 )));
93 } 97 }
94} 98}
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/parser/antlr/TransitiveClosureParserTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/parser/antlr/TransitiveClosureParserTest.java
index ed193e90..a9c5f62a 100644
--- a/subprojects/language/src/test/java/tools/refinery/language/tests/parser/antlr/TransitiveClosureParserTest.java
+++ b/subprojects/language/src/test/java/tools/refinery/language/tests/parser/antlr/TransitiveClosureParserTest.java
@@ -31,7 +31,7 @@ class TransitiveClosureParserTest {
31 var problem = parseHelper.parse(""" 31 var problem = parseHelper.parse("""
32 pred foo(a, b) <-> a + (b) > 10. 32 pred foo(a, b) <-> a + (b) > 10.
33 """); 33 """);
34 assertThat(problem.errors(), empty()); 34 assertThat(problem.getResourceErrors(), empty());
35 var literal = problem.pred("foo").conj(0).lit(0).get(); 35 var literal = problem.pred("foo").conj(0).lit(0).get();
36 assertThat(literal, instanceOf(ComparisonExpr.class)); 36 assertThat(literal, instanceOf(ComparisonExpr.class));
37 var left = ((ComparisonExpr) literal).getLeft(); 37 var left = ((ComparisonExpr) literal).getLeft();
@@ -45,7 +45,7 @@ class TransitiveClosureParserTest {
45 var problem = parseHelper.parse(""" 45 var problem = parseHelper.parse("""
46 pred foo(a, b) <-> equals+(a, b). 46 pred foo(a, b) <-> equals+(a, b).
47 """); 47 """);
48 assertThat(problem.errors(), empty()); 48 assertThat(problem.getResourceErrors(), empty());
49 var literal = problem.pred("foo").conj(0).lit(0).get(); 49 var literal = problem.pred("foo").conj(0).lit(0).get();
50 assertThat(literal, instanceOf(Atom.class)); 50 assertThat(literal, instanceOf(Atom.class));
51 var atom = (Atom) literal; 51 var atom = (Atom) literal;
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/rules/RuleParsingTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/rules/RuleParsingTest.java
index 68514bfa..56e65550 100644
--- a/subprojects/language/src/test/java/tools/refinery/language/tests/rules/RuleParsingTest.java
+++ b/subprojects/language/src/test/java/tools/refinery/language/tests/rules/RuleParsingTest.java
@@ -42,7 +42,7 @@ class RuleParsingTest {
42 """ }) 42 """ })
43 void simpleTest(String text) { 43 void simpleTest(String text) {
44 var problem = parseHelper.parse(text); 44 var problem = parseHelper.parse(text);
45 assertThat(problem.errors(), empty()); 45 assertThat(problem.getResourceErrors(), empty());
46 } 46 }
47 47
48 @Test 48 @Test
@@ -51,7 +51,7 @@ class RuleParsingTest {
51 pred Person(p). 51 pred Person(p).
52 rule r(p1): must Person(p1) ==> new p2, Person(p2) := unknown. 52 rule r(p1): must Person(p1) ==> new p2, Person(p2) := unknown.
53 """); 53 """);
54 assertThat(problem.errors(), empty()); 54 assertThat(problem.getResourceErrors(), empty());
55 assertThat(problem.rule("r").param(0), equalTo(problem.rule("r").conj(0).lit(0).arg(0).variable())); 55 assertThat(problem.rule("r").param(0), equalTo(problem.rule("r").conj(0).lit(0).arg(0).variable()));
56 assertThat(problem.rule("r").consequent(0).action(0).newVar(), 56 assertThat(problem.rule("r").consequent(0).action(0).newVar(),
57 equalTo(problem.rule("r").consequent(0).action(1).assertedAtom().arg(0).variable())); 57 equalTo(problem.rule("r").consequent(0).action(1).assertedAtom().arg(0).variable()));
@@ -63,7 +63,7 @@ class RuleParsingTest {
63 pred Friend(a, b). 63 pred Friend(a, b).
64 rule r(p1): !may Friend(p1, p2) ==> new p2, Friend(p1, p2) := true. 64 rule r(p1): !may Friend(p1, p2) ==> new p2, Friend(p1, p2) := true.
65 """); 65 """);
66 assertThat(problem.errors(), empty()); 66 assertThat(problem.getResourceErrors(), empty());
67 assertThat(problem.rule("r").conj(0).lit(0).negated().arg(1).variable(), 67 assertThat(problem.rule("r").conj(0).lit(0).negated().arg(1).variable(),
68 not(equalTo(problem.rule("r").consequent(0).action(1).assertedAtom().arg(1).variable()))); 68 not(equalTo(problem.rule("r").consequent(0).action(1).assertedAtom().arg(1).variable())));
69 } 69 }
@@ -74,7 +74,7 @@ class RuleParsingTest {
74 pred Friend(a, b). 74 pred Friend(a, b).
75 rule r(p1, p2): !may Friend(p1, p2) ==> new p2, Friend(p1, p2) := true. 75 rule r(p1, p2): !may Friend(p1, p2) ==> new p2, Friend(p1, p2) := true.
76 """); 76 """);
77 assertThat(problem.errors(), empty()); 77 assertThat(problem.getResourceErrors(), empty());
78 assertThat(problem.rule("r").param(1), 78 assertThat(problem.rule("r").param(1),
79 not(equalTo(problem.rule("r").consequent(0).action(1).assertedAtom().arg(1).variable()))); 79 not(equalTo(problem.rule("r").consequent(0).action(1).assertedAtom().arg(1).variable())));
80 } 80 }
@@ -85,6 +85,6 @@ class RuleParsingTest {
85 pred Person(p). 85 pred Person(p).
86 rule r(p1): must Friend(p1, p2) ==> delete p2. 86 rule r(p1): must Friend(p1, p2) ==> delete p2.
87 """); 87 """);
88 assertThat(problem.errors(), not(empty())); 88 assertThat(problem.getResourceErrors(), not(empty()));
89 } 89 }
90} 90}
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/scoping/NodeScopingTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/scoping/NodeScopingTest.java
index 734bfcd1..e76d2993 100644
--- a/subprojects/language/src/test/java/tools/refinery/language/tests/scoping/NodeScopingTest.java
+++ b/subprojects/language/src/test/java/tools/refinery/language/tests/scoping/NodeScopingTest.java
@@ -36,7 +36,7 @@ class NodeScopingTest {
36 var problem = parse(""" 36 var problem = parse("""
37 pred predicate({PARAM}node a). 37 pred predicate({PARAM}node a).
38 """, qualifiedNamePrefix); 38 """, qualifiedNamePrefix);
39 assertThat(problem.errors(), empty()); 39 assertThat(problem.getResourceErrors(), empty());
40 assertThat(problem.pred("predicate").param(0).getParameterType(), 40 assertThat(problem.pred("predicate").param(0).getParameterType(),
41 equalTo(problem.builtin().findClass("node").get())); 41 equalTo(problem.builtin().findClass("node").get()));
42 } 42 }
@@ -48,7 +48,7 @@ class NodeScopingTest {
48 predicate(a, a). 48 predicate(a, a).
49 ?predicate(a, b). 49 ?predicate(a, b).
50 """); 50 """);
51 assertThat(problem.errors(), empty()); 51 assertThat(problem.getResourceErrors(), empty());
52 assertThat(problem.nodeNames(), hasItems("a", "b")); 52 assertThat(problem.nodeNames(), hasItems("a", "b"));
53 assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.node("a"))); 53 assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.node("a")));
54 assertThat(problem.assertion(0).arg(1).node(), equalTo(problem.node("a"))); 54 assertThat(problem.assertion(0).arg(1).node(), equalTo(problem.node("a")));
@@ -62,7 +62,7 @@ class NodeScopingTest {
62 pred predicate(node a) <-> node(b). 62 pred predicate(node a) <-> node(b).
63 predicate(b). 63 predicate(b).
64 """); 64 """);
65 assertThat(problem.errors(), empty()); 65 assertThat(problem.getResourceErrors(), empty());
66 assertThat(problem.nodeNames(), hasItems("b")); 66 assertThat(problem.nodeNames(), hasItems("b"));
67 assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), equalTo(problem.node("b"))); 67 assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), equalTo(problem.node("b")));
68 assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.node("b"))); 68 assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.node("b")));
@@ -77,7 +77,7 @@ class NodeScopingTest {
77 predicate({PARAM}a, {PARAM}a). 77 predicate({PARAM}a, {PARAM}a).
78 ?predicate({PARAM}a, {PARAM}b). 78 ?predicate({PARAM}a, {PARAM}b).
79 """, qualifiedNamePrefix, namedProblem); 79 """, qualifiedNamePrefix, namedProblem);
80 assertThat(problem.errors(), empty()); 80 assertThat(problem.getResourceErrors(), empty());
81 assertThat(problem.nodeNames(), empty()); 81 assertThat(problem.nodeNames(), empty());
82 assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.individualNode("a"))); 82 assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.individualNode("a")));
83 assertThat(problem.assertion(0).arg(1).node(), equalTo(problem.individualNode("a"))); 83 assertThat(problem.assertion(0).arg(1).node(), equalTo(problem.individualNode("a")));
@@ -92,7 +92,7 @@ class NodeScopingTest {
92 indiv b. 92 indiv b.
93 pred predicate(node a) <-> node({PARAM}b). 93 pred predicate(node a) <-> node({PARAM}b).
94 """); 94 """);
95 assertThat(problem.errors(), empty()); 95 assertThat(problem.getResourceErrors(), empty());
96 assertThat(problem.nodeNames(), empty()); 96 assertThat(problem.nodeNames(), empty());
97 assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), equalTo(problem.individualNode("b"))); 97 assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), equalTo(problem.individualNode("b")));
98 } 98 }
@@ -109,7 +109,7 @@ class NodeScopingTest {
109 pred predicate(node x) <-> node(x). 109 pred predicate(node x) <-> node(x).
110 predicate({PARAM}). 110 predicate({PARAM}).
111 """, qualifiedName); 111 """, qualifiedName);
112 assertThat(problem.errors(), empty()); 112 assertThat(problem.getResourceErrors(), empty());
113 assertThat(problem.nodeNames(), empty()); 113 assertThat(problem.nodeNames(), empty());
114 assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.builtin().findClass("int").get().getNewNode())); 114 assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.builtin().findClass("int").get().getNewNode()));
115 } 115 }
@@ -121,7 +121,7 @@ class NodeScopingTest {
121 var problem = parse(""" 121 var problem = parse("""
122 pred predicate(node x) <-> node({PARAM}). 122 pred predicate(node x) <-> node({PARAM}).
123 """, qualifiedName); 123 """, qualifiedName);
124 assertThat(problem.errors(), empty()); 124 assertThat(problem.getResourceErrors(), empty());
125 assertThat(problem.nodeNames(), empty()); 125 assertThat(problem.nodeNames(), empty());
126 assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), 126 assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(),
127 equalTo(problem.builtin().findClass("int").get().getNewNode())); 127 equalTo(problem.builtin().findClass("int").get().getNewNode()));
@@ -139,7 +139,7 @@ class NodeScopingTest {
139 pred predicate(node x) <-> node(x). 139 pred predicate(node x) <-> node(x).
140 predicate({PARAM}). 140 predicate({PARAM}).
141 """, qualifiedName, namedProblem); 141 """, qualifiedName, namedProblem);
142 assertThat(problem.errors(), empty()); 142 assertThat(problem.getResourceErrors(), empty());
143 assertThat(problem.nodeNames(), empty()); 143 assertThat(problem.nodeNames(), empty());
144 assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.findClass("Foo").get().getNewNode())); 144 assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.findClass("Foo").get().getNewNode()));
145 } 145 }
@@ -151,7 +151,7 @@ class NodeScopingTest {
151 class Foo. 151 class Foo.
152 pred predicate(node x) <-> node({PARAM}). 152 pred predicate(node x) <-> node({PARAM}).
153 """, qualifiedName, namedProblem); 153 """, qualifiedName, namedProblem);
154 assertThat(problem.errors(), empty()); 154 assertThat(problem.getResourceErrors(), empty());
155 assertThat(problem.nodeNames(), empty()); 155 assertThat(problem.nodeNames(), empty());
156 assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), 156 assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(),
157 equalTo(problem.findClass("Foo").get().getNewNode())); 157 equalTo(problem.findClass("Foo").get().getNewNode()));
@@ -169,7 +169,7 @@ class NodeScopingTest {
169 pred predicate(node x) <-> node(x). 169 pred predicate(node x) <-> node(x).
170 predicate(new). 170 predicate(new).
171 """); 171 """);
172 assertThat(problem.errors(), empty()); 172 assertThat(problem.getResourceErrors(), empty());
173 assertThat(problem.nodeNames(), hasItems("new")); 173 assertThat(problem.nodeNames(), hasItems("new"));
174 assertThat(problem.assertion(0).arg(0).node(), not(equalTo(problem.findClass("Foo").get().getNewNode()))); 174 assertThat(problem.assertion(0).arg(0).node(), not(equalTo(problem.findClass("Foo").get().getNewNode())));
175 } 175 }
@@ -182,7 +182,7 @@ class NodeScopingTest {
182 pred predicate(Foo a) <-> node(a). 182 pred predicate(Foo a) <-> node(a).
183 predicate({PARAM}). 183 predicate({PARAM}).
184 """, qualifiedName, namedProblem); 184 """, qualifiedName, namedProblem);
185 assertThat(problem.errors(), empty()); 185 assertThat(problem.getResourceErrors(), empty());
186 assertThat(problem.nodeNames(), empty()); 186 assertThat(problem.nodeNames(), empty());
187 assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.findEnum("Foo").literal("alpha"))); 187 assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.findEnum("Foo").literal("alpha")));
188 } 188 }
@@ -194,7 +194,7 @@ class NodeScopingTest {
194 enum Foo { alpha, beta } 194 enum Foo { alpha, beta }
195 pred predicate(Foo a) <-> node({PARAM}). 195 pred predicate(Foo a) <-> node({PARAM}).
196 """, qualifiedName, namedProblem); 196 """, qualifiedName, namedProblem);
197 assertThat(problem.errors(), empty()); 197 assertThat(problem.getResourceErrors(), empty());
198 assertThat(problem.nodeNames(), empty()); 198 assertThat(problem.nodeNames(), empty());
199 assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), 199 assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(),
200 equalTo(problem.findEnum("Foo").literal("alpha"))); 200 equalTo(problem.findEnum("Foo").literal("alpha")));
@@ -214,7 +214,7 @@ class NodeScopingTest {
214 pred predicate(node a) <-> node(a). 214 pred predicate(node a) <-> node(a).
215 predicate({PARAM}). 215 predicate({PARAM}).
216 """, qualifiedName); 216 """, qualifiedName);
217 assertThat(problem.errors(), empty()); 217 assertThat(problem.getResourceErrors(), empty());
218 assertThat(problem.nodeNames(), empty()); 218 assertThat(problem.nodeNames(), empty());
219 assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.builtin().findEnum("bool").literal("true"))); 219 assertThat(problem.assertion(0).arg(0).node(), equalTo(problem.builtin().findEnum("bool").literal("true")));
220 } 220 }
@@ -226,7 +226,7 @@ class NodeScopingTest {
226 var problem = parse(""" 226 var problem = parse("""
227 pred predicate() <-> node({PARAM}). 227 pred predicate() <-> node({PARAM}).
228 """, qualifiedName); 228 """, qualifiedName);
229 assertThat(problem.errors(), empty()); 229 assertThat(problem.getResourceErrors(), empty());
230 assertThat(problem.nodeNames(), empty()); 230 assertThat(problem.nodeNames(), empty());
231 assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(), 231 assertThat(problem.pred("predicate").conj(0).lit(0).arg(0).node(),
232 equalTo(problem.builtin().findEnum("bool").literal("true"))); 232 equalTo(problem.builtin().findEnum("bool").literal("true")));
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/validation/MultiplicityValidationTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/validation/MultiplicityValidationTest.java
new file mode 100644
index 00000000..a8bcb1a6
--- /dev/null
+++ b/subprojects/language/src/test/java/tools/refinery/language/tests/validation/MultiplicityValidationTest.java
@@ -0,0 +1,119 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.language.tests.validation;
7
8import com.google.inject.Inject;
9import org.eclipse.emf.common.util.Diagnostic;
10import org.eclipse.xtext.testing.InjectWith;
11import org.eclipse.xtext.testing.extensions.InjectionExtension;
12import org.junit.jupiter.api.extension.ExtendWith;
13import org.junit.jupiter.params.ParameterizedTest;
14import org.junit.jupiter.params.provider.ValueSource;
15import tools.refinery.language.model.tests.utils.ProblemParseHelper;
16import tools.refinery.language.tests.ProblemInjectorProvider;
17import tools.refinery.language.validation.ProblemValidator;
18
19import static org.hamcrest.MatcherAssert.assertThat;
20import static org.hamcrest.Matchers.*;
21
22@ExtendWith(InjectionExtension.class)
23@InjectWith(ProblemInjectorProvider.class)
24class MultiplicityValidationTest {
25 @Inject
26 private ProblemParseHelper parseHelper;
27
28 @ParameterizedTest
29 @ValueSource(strings = {"2..5", "2..2", "2..*", "2", ""})
30 void validReferenceMultiplicityTest(String range) {
31 var problem = parseHelper.parse("""
32 class Foo {
33 Bar[%s] bar
34 }
35
36 class Bar.
37 """.formatted(range));
38 assertThat(problem.validate(), empty());
39 }
40
41 @ParameterizedTest
42 @ValueSource(strings = {"2..5", "2..2", "2..*", "2", "0..0", "0"})
43 void validScopeMultiplicityTest(String range) {
44 var problem = parseHelper.parse("""
45 class Foo.
46
47 scope Foo = %s.
48 """.formatted(range));
49 assertThat(problem.validate(), empty());
50 }
51
52
53 @ParameterizedTest
54 @ValueSource(strings = {"0", "0..0"})
55 void zeroMReferenceMultiplicityTest(String range) {
56 var problem = parseHelper.parse("""
57 class Foo {
58 Bar[%s] bar
59 }
60
61 class Bar.
62 """.formatted(range));
63 assertThat(problem.validate(), hasItem(allOf(
64 hasProperty("severity", is(Diagnostic.WARNING)),
65 hasProperty("issueCode", is(ProblemValidator.ZERO_MULTIPLICITY_ISSUE))
66 )));
67 }
68
69 @ParameterizedTest
70 @ValueSource(strings = {
71 "container Bar bar opposite foo",
72 "container Bar[0..1] bar opposite foo",
73 "container Bar bar", // Invalid, but has valid multiplicity.
74 "container Bar[0..1] bar", // Invalid, but has valid multiplicity.
75 "Bar bar opposite foo",
76 "Bar[0..1] bar opposite foo"
77 })
78 void validContainerReference(String referenceText) {
79 var problem = parseHelper.parse("""
80 class Foo {
81 %s
82 }
83
84 class Bar {
85 contains Foo foo opposite bar
86 }
87 """.formatted(referenceText));
88 assertThat(problem.validate(), not(hasItem(hasProperty("issueCode",
89 is(ProblemValidator.INVALID_MULTIPLICITY_ISSUE)))));
90 }
91
92 @ParameterizedTest
93 @ValueSource(strings = {
94 "container Bar[1] bar opposite foo",
95 "container Bar[1..2] bar opposite foo",
96 "container Bar[] bar opposite foo",
97 "container Bar[1] bar", // Also otherwise invalid, because the {@code opposite} is missing.
98 "container Bar[1..2] bar", // Also otherwise invalid, because the {@code opposite} is missing.
99 "container Bar[] bar", // Also otherwise invalid, because the {@code opposite} is missing.
100 "Bar[1] bar opposite foo",
101 "Bar[1..2] bar opposite foo",
102 "Bar[] bar opposite foo"
103 })
104 void invalidContainerReference(String referenceText) {
105 var problem = parseHelper.parse("""
106 class Foo {
107 %s
108 }
109
110 class Bar {
111 contains Foo foo opposite bar
112 }
113 """.formatted(referenceText));
114 assertThat(problem.validate(), hasItem(allOf(
115 hasProperty("severity", is(Diagnostic.ERROR)),
116 hasProperty("issueCode", is(ProblemValidator.INVALID_MULTIPLICITY_ISSUE))
117 )));
118 }
119}
diff --git a/subprojects/language/src/test/java/tools/refinery/language/tests/validation/OppositeValidationTest.java b/subprojects/language/src/test/java/tools/refinery/language/tests/validation/OppositeValidationTest.java
new file mode 100644
index 00000000..57602377
--- /dev/null
+++ b/subprojects/language/src/test/java/tools/refinery/language/tests/validation/OppositeValidationTest.java
@@ -0,0 +1,209 @@
1/*
2 * SPDX-FileCopyrightText: 2023 The Refinery Authors <https://refinery.tools/>
3 *
4 * SPDX-License-Identifier: EPL-2.0
5 */
6package tools.refinery.language.tests.validation;
7
8import com.google.inject.Inject;
9import org.eclipse.emf.common.util.Diagnostic;
10import org.eclipse.xtext.testing.InjectWith;
11import org.eclipse.xtext.testing.extensions.InjectionExtension;
12import org.junit.jupiter.api.Test;
13import org.junit.jupiter.api.extension.ExtendWith;
14import org.junit.jupiter.params.ParameterizedTest;
15import org.junit.jupiter.params.provider.ValueSource;
16import tools.refinery.language.model.tests.utils.ProblemParseHelper;
17import tools.refinery.language.tests.ProblemInjectorProvider;
18import tools.refinery.language.validation.ProblemValidator;
19
20import java.util.Set;
21
22import static org.hamcrest.MatcherAssert.assertThat;
23import static org.hamcrest.Matchers.*;
24
25@ExtendWith(InjectionExtension.class)
26@InjectWith(ProblemInjectorProvider.class)
27class OppositeValidationTest {
28 @Inject
29 private ProblemParseHelper parseHelper;
30
31 @ParameterizedTest
32 @ValueSource(strings = {"""
33 class Foo {
34 Bar bar opposite foo
35 }
36
37 class Bar {
38 Foo foo opposite bar
39 }
40 """, """
41 class Foo {
42 contains Bar bar opposite foo
43 }
44
45 class Bar {
46 Foo foo opposite bar
47 }
48 """, """
49 class Foo {
50 contains Bar bar opposite foo
51 }
52
53 class Bar {
54 container Foo foo opposite bar
55 }
56 """, """
57 class Foo {
58 Foo foo[] opposite foo
59 }
60 """})
61 void validOppositeTest(String text) {
62 var problem = parseHelper.parse(text);
63 var issues = problem.validate();
64 assertThat(issues, not(hasItems(hasProperty("issueCode", in(Set.of(
65 ProblemValidator.INVALID_OPPOSITE_ISSUE,
66 ProblemValidator.MISSING_OPPOSITE_ISSUE
67 ))))));
68 }
69
70 @Test
71 void missingOppositeTest() {
72 var problem = parseHelper.parse("""
73 class Foo {
74 Bar bar opposite foo
75 }
76
77 class Bar {
78 Foo foo
79 }
80 """);
81 var issues = problem.validate();
82 assertThat(issues, hasItems(allOf(
83 hasProperty("severity", is(Diagnostic.ERROR)),
84 hasProperty("issueCode", is(ProblemValidator.INVALID_OPPOSITE_ISSUE)),
85 hasProperty("message", stringContainsInOrder("foo", "bar"))
86 ), allOf(
87 hasProperty("severity", is(Diagnostic.ERROR)),
88 hasProperty("issueCode", is(ProblemValidator.MISSING_OPPOSITE_ISSUE)),
89 hasProperty("message", stringContainsInOrder("bar", "foo"))
90 )));
91 }
92
93 @Test
94 void oppositeMismatchTest() {
95 var problem = parseHelper.parse("""
96 class Foo {
97 Bar bar opposite foo
98 Bar quux opposite foo
99 }
100
101 class Bar {
102 Foo foo opposite bar
103 }
104 """);
105 var issues = problem.validate();
106 assertThat(issues, hasItem(allOf(
107 hasProperty("severity", is(Diagnostic.ERROR)),
108 hasProperty("issueCode", is(ProblemValidator.INVALID_OPPOSITE_ISSUE)),
109 hasProperty("message", stringContainsInOrder("foo", "quux", "bar"))
110 )));
111 }
112
113 @Test
114 void oppositeMismatchProxyTest() {
115 var problem = parseHelper.parse("""
116 class Foo {
117 Bar bar opposite foo
118 }
119
120 class Bar {
121 Foo foo opposite quux
122 }
123 """);
124 var issues = problem.validate();
125 assertThat(issues, hasItem(allOf(
126 hasProperty("severity", is(Diagnostic.ERROR)),
127 hasProperty("issueCode", is(ProblemValidator.INVALID_OPPOSITE_ISSUE)),
128 hasProperty("message", allOf(
129 stringContainsInOrder("foo", "bar"),
130 not(containsString("null"))
131 ))
132 )));
133 }
134
135 @ParameterizedTest
136 @ValueSource(strings = {"contains", "container"})
137 void containmentWithProxyOppositeTest(String keyword) {
138 var problem = parseHelper.parse("""
139 class Foo {
140 %s Bar bar opposite foo
141 }
142
143 class Bar.
144 """.formatted(keyword));
145 var issues = problem.validate();
146 assertThat(issues, not(hasItem(hasProperty("issueCode",
147 is(ProblemValidator.INVALID_OPPOSITE_ISSUE)))));
148 }
149
150 @Test
151 void containmentWithContainmentOppositeTest() {
152 var problem = parseHelper.parse("""
153 class Foo {
154 contains Bar bar opposite foo
155 }
156
157 class Bar {
158 contains Foo foo opposite bar
159 }
160 """);
161 var issues = problem.validate();
162 assertThat(issues, hasItems(allOf(
163 hasProperty("severity", is(Diagnostic.ERROR)),
164 hasProperty("issueCode", is(ProblemValidator.INVALID_OPPOSITE_ISSUE)),
165 hasProperty("message", stringContainsInOrder("foo", "bar"))
166 ), allOf(
167 hasProperty("severity", is(Diagnostic.ERROR)),
168 hasProperty("issueCode", is(ProblemValidator.INVALID_OPPOSITE_ISSUE)),
169 hasProperty("message", stringContainsInOrder("foo", "bar"))
170 )));
171 }
172
173 @Test
174 void containerWithoutOppositeTest() {
175 var problem = parseHelper.parse("""
176 class Foo {
177 container Bar bar
178 }
179
180 class Bar.
181 """);
182 var issues = problem.validate();
183 assertThat(issues, hasItem(allOf(
184 hasProperty("severity", is(Diagnostic.ERROR)),
185 hasProperty("issueCode", is(ProblemValidator.MISSING_OPPOSITE_ISSUE)),
186 hasProperty("message", containsString("bar"))
187 )));
188 }
189
190 @ParameterizedTest
191 @ValueSource(strings = {"Foo foo", "container Foo foo"})
192 void containerInvalidOppositeTest(String reference) {
193 var problem = parseHelper.parse("""
194 class Foo {
195 container Bar bar opposite foo
196 }
197
198 class Bar {
199 %s opposite bar
200 }
201 """.formatted(reference));
202 var issues = problem.validate();
203 assertThat(issues, hasItem(allOf(
204 hasProperty("severity", is(Diagnostic.ERROR)),
205 hasProperty("issueCode", is(ProblemValidator.INVALID_OPPOSITE_ISSUE)),
206 hasProperty("message", stringContainsInOrder("foo", "bar"))
207 )));
208 }
209}
diff --git a/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/ProblemParseHelper.java b/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/ProblemParseHelper.java
index 6f6a87f7..f1535716 100644
--- a/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/ProblemParseHelper.java
+++ b/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/ProblemParseHelper.java
@@ -5,23 +5,26 @@
5 */ 5 */
6package tools.refinery.language.model.tests.utils; 6package tools.refinery.language.model.tests.utils;
7 7
8import org.eclipse.xtext.testing.util.ParseHelper;
9
10import com.google.inject.Inject; 8import com.google.inject.Inject;
11 9import org.eclipse.emf.ecore.util.EcoreUtil;
10import org.eclipse.xtext.testing.util.ParseHelper;
11import org.eclipse.xtext.validation.IResourceValidator;
12import tools.refinery.language.model.problem.Problem; 12import tools.refinery.language.model.problem.Problem;
13 13
14public class ProblemParseHelper { 14public class ProblemParseHelper {
15 @Inject 15 @Inject
16 private IResourceValidator resourceValidator;
17 @Inject
16 private ParseHelper<Problem> parseHelper; 18 private ParseHelper<Problem> parseHelper;
17 19
18 public WrappedProblem parse(String text) { 20 public WrappedProblem parse(String text) {
19 Problem problem; 21 Problem problem;
20 try { 22 try {
21 problem = parseHelper.parse(text); 23 problem = parseHelper.parse(text);
22 } catch (Exception e) { 24 } catch (Exception e) {
23 throw new RuntimeException("Unexpected exception while parsing Problem", e); 25 throw new AssertionError("Unexpected exception while parsing Problem", e);
24 } 26 }
27 EcoreUtil.resolveAll(problem);
25 return new WrappedProblem(problem); 28 return new WrappedProblem(problem);
26 } 29 }
27} 30}
diff --git a/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/WrappedProblem.java b/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/WrappedProblem.java
index e5aa0043..fc51ff57 100644
--- a/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/WrappedProblem.java
+++ b/subprojects/language/src/testFixtures/java/tools/refinery/language/model/tests/utils/WrappedProblem.java
@@ -6,7 +6,7 @@
6package tools.refinery.language.model.tests.utils; 6package tools.refinery.language.model.tests.utils;
7 7
8import org.eclipse.emf.ecore.resource.Resource.Diagnostic; 8import org.eclipse.emf.ecore.resource.Resource.Diagnostic;
9import org.eclipse.emf.ecore.util.EcoreUtil; 9import org.eclipse.emf.ecore.util.Diagnostician;
10import tools.refinery.language.model.problem.*; 10import tools.refinery.language.model.problem.*;
11import tools.refinery.language.utils.BuiltinSymbols; 11import tools.refinery.language.utils.BuiltinSymbols;
12import tools.refinery.language.utils.ProblemDesugarer; 12import tools.refinery.language.utils.ProblemDesugarer;
@@ -19,11 +19,18 @@ public record WrappedProblem(Problem problem) {
19 return problem; 19 return problem;
20 } 20 }
21 21
22 public List<Diagnostic> errors() { 22 public List<Diagnostic> getResourceErrors() {
23 EcoreUtil.resolveAll(problem);
24 return problem.eResource().getErrors(); 23 return problem.eResource().getErrors();
25 } 24 }
26 25
26 public List<Diagnostic> getResourceWarnings() {
27 return problem.eResource().getWarnings();
28 }
29
30 public List<org.eclipse.emf.common.util.Diagnostic> validate() {
31 return Diagnostician.INSTANCE.validate(problem).getChildren();
32 }
33
27 public WrappedProblem builtin() { 34 public WrappedProblem builtin() {
28 return new WrappedProblem(new ProblemDesugarer().getBuiltinProblem(problem).orElseThrow()); 35 return new WrappedProblem(new ProblemDesugarer().getBuiltinProblem(problem).orElseThrow());
29 } 36 }