From 2599ceeccbcfb531b3a95427b1b8904caf102242 Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Thu, 15 Jun 2023 20:41:09 +0200 Subject: refactor(query): structural equality matcher Add the ability to create assertions without pre-processing Dnf clauses (raw matchin mode). Also fix tests broken by Dnf pre-processing. --- .../java/tools/refinery/store/query/dnf/Dnf.java | 5 + .../query/equality/DeepDnfEqualityChecker.java | 42 ++++ .../dnf/DnfBuilderVariableUnificationTest.java | 234 +++++++++++++-------- .../query/tests/StructurallyEqualToRawTest.java | 189 +++++++++++++++++ .../store/query/tests/StructurallyEqualToTest.java | 93 +++++++- .../MismatchDescribingDnfEqualityChecker.java | 32 ++- .../refinery/store/query/tests/QueryMatchers.java | 27 +++ .../store/query/tests/StructurallyEqualTo.java | 2 +- .../store/query/tests/StructurallyEqualToRaw.java | 51 +++++ 9 files changed, 568 insertions(+), 107 deletions(-) create mode 100644 subprojects/store-query/src/test/java/tools/refinery/store/query/tests/StructurallyEqualToRawTest.java create mode 100644 subprojects/store-query/src/testFixtures/java/tools/refinery/store/query/tests/StructurallyEqualToRaw.java (limited to 'subprojects') diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/Dnf.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/Dnf.java index 64790f42..c5b51b81 100644 --- a/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/Dnf.java +++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/dnf/Dnf.java @@ -119,6 +119,11 @@ public final class Dnf implements Constraint { if (arity() != other.arity()) { return false; } + for (int i = 0; i < arity(); i++) { + if (!symbolicParameters.get(i).getDirection().equals(other.getSymbolicParameters().get(i).getDirection())) { + return false; + } + } int numClauses = clauses.size(); if (numClauses != other.clauses.size()) { return false; diff --git a/subprojects/store-query/src/main/java/tools/refinery/store/query/equality/DeepDnfEqualityChecker.java b/subprojects/store-query/src/main/java/tools/refinery/store/query/equality/DeepDnfEqualityChecker.java index bb60ccc9..1eeb5723 100644 --- a/subprojects/store-query/src/main/java/tools/refinery/store/query/equality/DeepDnfEqualityChecker.java +++ b/subprojects/store-query/src/main/java/tools/refinery/store/query/equality/DeepDnfEqualityChecker.java @@ -6,6 +6,9 @@ package tools.refinery.store.query.equality; import tools.refinery.store.query.dnf.Dnf; +import tools.refinery.store.query.dnf.DnfClause; +import tools.refinery.store.query.dnf.SymbolicParameter; +import tools.refinery.store.query.literal.Literal; import tools.refinery.store.util.CycleDetectingMapper; import java.util.List; @@ -19,6 +22,45 @@ public class DeepDnfEqualityChecker implements DnfEqualityChecker { return mapper.map(new Pair(left, right)); } + public boolean dnfEqualRaw(List symbolicParameters, + List> clauses, Dnf other) { + int arity = symbolicParameters.size(); + if (arity != other.arity()) { + return false; + } + for (int i = 0; i < arity; i++) { + if (!symbolicParameters.get(i).getDirection().equals(other.getSymbolicParameters().get(i).getDirection())) { + return false; + } + } + int numClauses = clauses.size(); + if (numClauses != other.getClauses().size()) { + return false; + } + for (int i = 0; i < numClauses; i++) { + var literalEqualityHelper = new LiteralEqualityHelper(this, symbolicParameters, + other.getSymbolicParameters()); + if (!equalsWithSubstitutionRaw(literalEqualityHelper, clauses.get(i), other.getClauses().get(i))) { + return false; + } + } + return true; + } + + private boolean equalsWithSubstitutionRaw(LiteralEqualityHelper helper, List literals, + DnfClause other) { + int size = literals.size(); + if (size != other.literals().size()) { + return false; + } + for (int i = 0; i < size; i++) { + if (!literals.get(i).equalsWithSubstitution(helper, other.literals().get(i))) { + return false; + } + } + return true; + } + protected boolean doCheckEqual(Pair pair) { return pair.left.equalsWithSubstitution(this, pair.right); } diff --git a/subprojects/store-query/src/test/java/tools/refinery/store/query/dnf/DnfBuilderVariableUnificationTest.java b/subprojects/store-query/src/test/java/tools/refinery/store/query/dnf/DnfBuilderVariableUnificationTest.java index a54ad4d6..4a85fe32 100644 --- a/subprojects/store-query/src/test/java/tools/refinery/store/query/dnf/DnfBuilderVariableUnificationTest.java +++ b/subprojects/store-query/src/test/java/tools/refinery/store/query/dnf/DnfBuilderVariableUnificationTest.java @@ -6,6 +6,8 @@ package tools.refinery.store.query.dnf; import org.junit.jupiter.api.Test; +import tools.refinery.store.query.term.ParameterDirection; +import tools.refinery.store.query.term.Variable; import tools.refinery.store.query.view.KeyOnlyView; import tools.refinery.store.query.view.SymbolView; import tools.refinery.store.representation.Symbol; @@ -17,7 +19,7 @@ import static tools.refinery.store.query.tests.QueryMatchers.structurallyEqualTo class DnfBuilderVariableUnificationTest { private final Symbol friend = new Symbol<>("friend", 2, Boolean.class, false); - private final Symbol children = new Symbol<>("friend", 2, Boolean.class, false); + private final Symbol children = new Symbol<>("children", 2, Boolean.class, false); private final SymbolView friendView = new KeyOnlyView<>(friend); private final SymbolView childrenView = new KeyOnlyView<>(children); @@ -30,12 +32,14 @@ class DnfBuilderVariableUnificationTest { p.isEquivalent(q) )); }); - var expected = Dnf.of(builder -> { - var p = builder.parameter("p"); - builder.clause(friendView.call(p, p)); - }); - assertThat(actual, structurallyEqualTo(expected)); + var expectedP = Variable.of("p"); + assertThat(actual, structurallyEqualTo( + List.of(new SymbolicParameter(expectedP, ParameterDirection.OUT)), + List.of( + List.of(friendView.call(expectedP, expectedP)) + ) + )); } @Test @@ -47,12 +51,14 @@ class DnfBuilderVariableUnificationTest { q.isEquivalent(p) )); }); - var expected = Dnf.of(builder -> { - var p = builder.parameter("p"); - builder.clause(friendView.call(p, p)); - }); - assertThat(actual, structurallyEqualTo(expected)); + var expectedP = Variable.of("p"); + assertThat(actual, structurallyEqualTo( + List.of(new SymbolicParameter(expectedP, ParameterDirection.OUT)), + List.of( + List.of(friendView.call(expectedP, expectedP)) + ) + )); } @Test @@ -61,9 +67,14 @@ class DnfBuilderVariableUnificationTest { friendView.call(p, q), p.isEquivalent(q) ))); - var expected = Dnf.of(builder -> builder.clause(p -> List.of(friendView.call(p, p)))); - assertThat(actual, structurallyEqualTo(expected)); + var expectedP = Variable.of("p"); + assertThat(actual, structurallyEqualTo( + List.of(), + List.of( + List.of(friendView.call(expectedP, expectedP)) + ) + )); } @Test @@ -74,12 +85,14 @@ class DnfBuilderVariableUnificationTest { childrenView.call(p, r), q.isEquivalent(r) ))); - var expected = Dnf.of(builder -> builder.clause(p -> List.of( - friendView.call(p, p), - childrenView.call(p, p) - ))); - assertThat(actual, structurallyEqualTo(expected)); + var expectedP = Variable.of("p"); + assertThat(actual, structurallyEqualTo( + List.of(), + List.of( + List.of(friendView.call(expectedP, expectedP), childrenView.call(expectedP, expectedP)) + ) + )); } @Test @@ -90,9 +103,14 @@ class DnfBuilderVariableUnificationTest { friendView.call(p, r), q.isEquivalent(r) ))); - var expected = Dnf.of(builder -> builder.clause(p -> List.of(friendView.call(p, p)))); - assertThat(actual, structurallyEqualTo(expected)); + var expectedP = Variable.of("p"); + assertThat(actual, structurallyEqualTo( + List.of(), + List.of( + List.of(friendView.call(expectedP, expectedP)) + ) + )); } @Test @@ -105,16 +123,18 @@ class DnfBuilderVariableUnificationTest { p.isEquivalent(q) ); }); - var expected = Dnf.of(builder -> { - var p = builder.parameter("p"); - var q = builder.parameter("q"); - builder.clause( - q.isEquivalent(p), - friendView.call(p, p) - ); - }); - assertThat(actual, structurallyEqualTo(expected)); + var expectedP = Variable.of("p"); + var expectedQ = Variable.of("q"); + assertThat(actual, structurallyEqualTo( + List.of( + new SymbolicParameter(expectedP, ParameterDirection.OUT), + new SymbolicParameter(expectedQ, ParameterDirection.OUT) + ), + List.of( + List.of(friendView.call(expectedP, expectedP), expectedQ.isEquivalent(expectedP)) + ) + )); } @Test @@ -130,19 +150,25 @@ class DnfBuilderVariableUnificationTest { r.isEquivalent(q) ); }); - var expected = Dnf.of(builder -> { - var p = builder.parameter("p"); - var q = builder.parameter("q"); - var r = builder.parameter("r"); - builder.clause( - q.isEquivalent(p), - r.isEquivalent(p), - friendView.call(p, p), - childrenView.call(p, p) - ); - }); - assertThat(actual, structurallyEqualTo(expected)); + var expectedP = Variable.of("p"); + var expectedQ = Variable.of("q"); + var expectedR = Variable.of("r"); + assertThat(actual, structurallyEqualTo( + List.of( + new SymbolicParameter(expectedP, ParameterDirection.OUT), + new SymbolicParameter(expectedQ, ParameterDirection.OUT), + new SymbolicParameter(expectedR, ParameterDirection.OUT) + ), + List.of( + List.of( + friendView.call(expectedP, expectedP), + expectedQ.isEquivalent(expectedP), + expectedR.isEquivalent(expectedP), + childrenView.call(expectedP, expectedP) + ) + ) + )); } @Test @@ -157,17 +183,23 @@ class DnfBuilderVariableUnificationTest { q.isEquivalent(r) )); }); - var expected = Dnf.of(builder -> { - var p = builder.parameter("p"); - var q = builder.parameter("q"); - builder.clause( - q.isEquivalent(p), - friendView.call(p, p), - childrenView.call(p, p) - ); - }); - assertThat(actual, structurallyEqualTo(expected)); + + var expectedP = Variable.of("p"); + var expectedQ = Variable.of("q"); + assertThat(actual, structurallyEqualTo( + List.of( + new SymbolicParameter(expectedP, ParameterDirection.OUT), + new SymbolicParameter(expectedQ, ParameterDirection.OUT) + ), + List.of( + List.of( + friendView.call(expectedP, expectedP), + expectedQ.isEquivalent(expectedP), + childrenView.call(expectedP, expectedP) + ) + ) + )); } @Test @@ -182,17 +214,22 @@ class DnfBuilderVariableUnificationTest { q.isEquivalent(r) )); }); - var expected = Dnf.of(builder -> { - var p = builder.parameter("p"); - var q = builder.parameter("q"); - builder.clause( - q.isEquivalent(p), - friendView.call(p, p), - childrenView.call(p, p) - ); - }); - assertThat(actual, structurallyEqualTo(expected)); + var expectedP = Variable.of("p"); + var expectedQ = Variable.of("q"); + assertThat(actual, structurallyEqualTo( + List.of( + new SymbolicParameter(expectedP, ParameterDirection.OUT), + new SymbolicParameter(expectedQ, ParameterDirection.OUT) + ), + List.of( + List.of( + friendView.call(expectedP, expectedP), + expectedQ.isEquivalent(expectedP), + childrenView.call(expectedP, expectedP) + ) + ) + )); } @Test @@ -207,17 +244,22 @@ class DnfBuilderVariableUnificationTest { r.isEquivalent(q) )); }); - var expected = Dnf.of(builder -> { - var p = builder.parameter("p"); - var q = builder.parameter("q"); - builder.clause( - q.isEquivalent(p), - friendView.call(p, p), - childrenView.call(p, p) - ); - }); - assertThat(actual, structurallyEqualTo(expected)); + var expectedP = Variable.of("p"); + var expectedQ = Variable.of("q"); + assertThat(actual, structurallyEqualTo( + List.of( + new SymbolicParameter(expectedP, ParameterDirection.OUT), + new SymbolicParameter(expectedQ, ParameterDirection.OUT) + ), + List.of( + List.of( + friendView.call(expectedP, expectedP), + expectedQ.isEquivalent(expectedP), + childrenView.call(expectedP, expectedP) + ) + ) + )); } @Test @@ -232,17 +274,22 @@ class DnfBuilderVariableUnificationTest { r.isEquivalent(q) )); }); - var expected = Dnf.of(builder -> { - var p = builder.parameter("p"); - var q = builder.parameter("q"); - builder.clause( - q.isEquivalent(p), - friendView.call(p, p), - childrenView.call(p, p) - ); - }); - assertThat(actual, structurallyEqualTo(expected)); + var expectedP = Variable.of("p"); + var expectedQ = Variable.of("q"); + assertThat(actual, structurallyEqualTo( + List.of( + new SymbolicParameter(expectedP, ParameterDirection.OUT), + new SymbolicParameter(expectedQ, ParameterDirection.OUT) + ), + List.of( + List.of( + friendView.call(expectedP, expectedP), + expectedQ.isEquivalent(expectedP), + childrenView.call(expectedP, expectedP) + ) + ) + )); } @Test @@ -258,16 +305,21 @@ class DnfBuilderVariableUnificationTest { q.isEquivalent(s) )); }); - var expected = Dnf.of(builder -> { - var p = builder.parameter("p"); - var q = builder.parameter("q"); - builder.clause( - q.isEquivalent(p), - friendView.call(p, p), - childrenView.call(p, p) - ); - }); - assertThat(actual, structurallyEqualTo(expected)); + var expectedP = Variable.of("p"); + var expectedQ = Variable.of("q"); + assertThat(actual, structurallyEqualTo( + List.of( + new SymbolicParameter(expectedP, ParameterDirection.OUT), + new SymbolicParameter(expectedQ, ParameterDirection.OUT) + ), + List.of( + List.of( + friendView.call(expectedP, expectedP), + expectedQ.isEquivalent(expectedP), + childrenView.call(expectedP, expectedP) + ) + ) + )); } } diff --git a/subprojects/store-query/src/test/java/tools/refinery/store/query/tests/StructurallyEqualToRawTest.java b/subprojects/store-query/src/test/java/tools/refinery/store/query/tests/StructurallyEqualToRawTest.java new file mode 100644 index 00000000..07a55ff3 --- /dev/null +++ b/subprojects/store-query/src/test/java/tools/refinery/store/query/tests/StructurallyEqualToRawTest.java @@ -0,0 +1,189 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.store.query.tests; + +import org.junit.jupiter.api.Test; +import tools.refinery.store.query.dnf.Dnf; +import tools.refinery.store.query.dnf.SymbolicParameter; +import tools.refinery.store.query.term.ParameterDirection; +import tools.refinery.store.query.term.Variable; +import tools.refinery.store.query.view.KeyOnlyView; +import tools.refinery.store.representation.Symbol; + +import java.util.List; + +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static tools.refinery.store.query.tests.QueryMatchers.structurallyEqualTo; + +class StructurallyEqualToRawTest { + @Test + void flatEqualsTest() { + var p = Variable.of("p"); + var q = Variable.of("q"); + var person = new Symbol<>("Person", 1, Boolean.class, false); + var personView = new KeyOnlyView<>(person); + + var actual = Dnf.builder("Actual").parameters(p).clause(personView.call(p)).build(); + + assertThat(actual, structurallyEqualTo( + List.of(new SymbolicParameter(q, ParameterDirection.OUT)), + List.of(List.of(personView.call(q))) + )); + } + + @Test + void flatNotEqualsTest() { + var p = Variable.of("p"); + var q = Variable.of("q"); + var friend = new Symbol<>("friend", 2, Boolean.class, false); + var friendView = new KeyOnlyView<>(friend); + + var actual = Dnf.builder("Actual").parameters(p).clause(friendView.call(p, q)).build(); + + var assertion = structurallyEqualTo( + List.of(new SymbolicParameter(q, ParameterDirection.OUT)), + List.of(List.of(friendView.call(q, q))) + ); + assertThrows(AssertionError.class, () -> assertThat(actual, assertion)); + } + + @Test + void deepEqualsTest() { + var p = Variable.of("p"); + var q = Variable.of("q"); + var person = new Symbol<>("Person", 1, Boolean.class, false); + var personView = new KeyOnlyView<>(person); + + var actual = Dnf.builder("Actual").parameters(q).clause( + Dnf.builder("Actual2").parameters(p).clause(personView.call(p)).build().call(q) + ).build(); + + assertThat(actual, structurallyEqualTo( + List.of(new SymbolicParameter(q, ParameterDirection.OUT)), + List.of( + List.of( + Dnf.builder("Expected2").parameters(p).clause(personView.call(p)).build().call(q) + ) + ) + )); + } + + @Test + void deepNotEqualsTest() { + var p = Variable.of("p"); + var q = Variable.of("q"); + var friend = new Symbol<>("friend", 2, Boolean.class, false); + var friendView = new KeyOnlyView<>(friend); + + var actual = Dnf.builder("Actual").parameter(q).clause( + Dnf.builder("Actual2").parameters(p).clause(friendView.call(p, q)).build().call(q) + ).build(); + + var assertion = structurallyEqualTo( + List.of(new SymbolicParameter(q, ParameterDirection.OUT)), + List.of( + List.of( + Dnf.builder("Expected2") + .parameters(p) + .clause(friendView.call(p, p)) + .build() + .call(q) + ) + ) + ); + var error = assertThrows(AssertionError.class, () -> assertThat(actual, assertion)); + assertThat(error.getMessage(), allOf(containsString("Expected2"), containsString("Actual2"))); + } + + @Test + void parameterListLengthMismatchTest() { + var p = Variable.of("p"); + var q = Variable.of("q"); + var friend = new Symbol<>("friend", 2, Boolean.class, false); + var friendView = new KeyOnlyView<>(friend); + + var actual = Dnf.builder("Actual").parameters(p, q).clause( + friendView.call(p, q) + ).build(); + + var assertion = structurallyEqualTo( + List.of(new SymbolicParameter(p, ParameterDirection.OUT)), + List.of(List.of(friendView.call(p, p))) + ); + + assertThrows(AssertionError.class, () -> assertThat(actual, assertion)); + } + + @Test + void parameterDirectionMismatchTest() { + var p = Variable.of("p"); + var person = new Symbol<>("Person", 1, Boolean.class, false); + var personView = new KeyOnlyView<>(person); + + var actual = Dnf.builder("Actual").parameter(p, ParameterDirection.IN).clause( + personView.call(p) + ).build(); + + var assertion = structurallyEqualTo( + List.of(new SymbolicParameter(p, ParameterDirection.OUT)), + List.of(List.of(personView.call(p))) + ); + + assertThrows(AssertionError.class, () -> assertThat(actual, assertion)); + } + + @Test + void clauseCountMismatchTest() { + var p = Variable.of("p"); + var q = Variable.of("q"); + var friend = new Symbol<>("friend", 2, Boolean.class, false); + var friendView = new KeyOnlyView<>(friend); + + var actual = Dnf.builder("Actual").parameters(p, q).clause( + friendView.call(p, q) + ).build(); + + var assertion = structurallyEqualTo( + List.of( + new SymbolicParameter(p, ParameterDirection.OUT), + new SymbolicParameter(q, ParameterDirection.OUT) + ), + List.of( + List.of(friendView.call(p, q)), + List.of(friendView.call(q, p)) + ) + ); + + assertThrows(AssertionError.class, () -> assertThat(actual, assertion)); + } + + @Test + void literalCountMismatchTest() { + var p = Variable.of("p"); + var q = Variable.of("q"); + var friend = new Symbol<>("friend", 2, Boolean.class, false); + var friendView = new KeyOnlyView<>(friend); + + var actual = Dnf.builder("Actual").parameters(p, q).clause( + friendView.call(p, q) + ).build(); + + var assertion = structurallyEqualTo( + List.of( + new SymbolicParameter(p, ParameterDirection.OUT), + new SymbolicParameter(q, ParameterDirection.OUT) + ), + List.of( + List.of(friendView.call(p, q), friendView.call(q, p)) + ) + ); + + assertThrows(AssertionError.class, () -> assertThat(actual, assertion)); + } +} diff --git a/subprojects/store-query/src/test/java/tools/refinery/store/query/tests/StructurallyEqualToTest.java b/subprojects/store-query/src/test/java/tools/refinery/store/query/tests/StructurallyEqualToTest.java index a1407288..e2983a3a 100644 --- a/subprojects/store-query/src/test/java/tools/refinery/store/query/tests/StructurallyEqualToTest.java +++ b/subprojects/store-query/src/test/java/tools/refinery/store/query/tests/StructurallyEqualToTest.java @@ -7,6 +7,7 @@ package tools.refinery.store.query.tests; import org.junit.jupiter.api.Test; import tools.refinery.store.query.dnf.Dnf; +import tools.refinery.store.query.term.ParameterDirection; import tools.refinery.store.query.term.Variable; import tools.refinery.store.query.view.KeyOnlyView; import tools.refinery.store.representation.Symbol; @@ -34,11 +35,11 @@ class StructurallyEqualToTest { void flatNotEqualsTest() { var p = Variable.of("p"); var q = Variable.of("q"); - var person = new Symbol<>("Person", 1, Boolean.class, false); - var personView = new KeyOnlyView<>(person); + var friend = new Symbol<>("friend", 2, Boolean.class, false); + var friendView = new KeyOnlyView<>(friend); - var expected = Dnf.builder("Expected").parameters(q).clause(personView.call(q)).build(); - var actual = Dnf.builder("Actual").parameters(p).clause(personView.call(q)).build(); + var expected = Dnf.builder("Expected").parameters(q).clause(friendView.call(q, q)).build(); + var actual = Dnf.builder("Actual").parameters(p).clause(friendView.call(p, q)).build(); var assertion = structurallyEqualTo(expected); assertThrows(AssertionError.class, () -> assertThat(actual, assertion)); @@ -65,18 +66,92 @@ class StructurallyEqualToTest { void deepNotEqualsTest() { var p = Variable.of("p"); var q = Variable.of("q"); - var person = new Symbol<>("Person", 1, Boolean.class, false); - var personView = new KeyOnlyView<>(person); + var friend = new Symbol<>("friend", 2, Boolean.class, false); + var friendView = new KeyOnlyView<>(friend); var expected = Dnf.builder("Expected").parameters(q).clause( - Dnf.builder("Expected2").parameters(p).clause(personView.call(p)).build().call(q) + Dnf.builder("Expected2").parameters(p).clause(friendView.call(p, p)).build().call(q) ).build(); - var actual = Dnf.builder("Actual").parameters(q).clause( - Dnf.builder("Actual2").parameters(p).clause(personView.call(q)).build().call(q) + var actual = Dnf.builder("Actual").parameter(q).clause( + Dnf.builder("Actual2").parameters(p).clause(friendView.call(p, q)).build().call(q) ).build(); var assertion = structurallyEqualTo(expected); var error = assertThrows(AssertionError.class, () -> assertThat(actual, assertion)); assertThat(error.getMessage(), containsString(" called from Expected/1 ")); } + + @Test + void parameterListLengthMismatchTest() { + var p = Variable.of("p"); + var q = Variable.of("q"); + var friend = new Symbol<>("friend", 2, Boolean.class, false); + var friendView = new KeyOnlyView<>(friend); + + var expected = Dnf.builder("Expected").parameter(p).clause( + friendView.call(p, p) + ).build(); + var actual = Dnf.builder("Actual").parameters(p, q).clause( + friendView.call(p, q) + ).build(); + + var assertion = structurallyEqualTo(expected); + assertThrows(AssertionError.class, () -> assertThat(actual, assertion)); + } + + @Test + void parameterDirectionMismatchTest() { + var p = Variable.of("p"); + var person = new Symbol<>("Person", 1, Boolean.class, false); + var personView = new KeyOnlyView<>(person); + + var expected = Dnf.builder("Expected").parameter(p, ParameterDirection.OUT).clause( + personView.call(p) + ).build(); + var actual = Dnf.builder("Actual").parameter(p, ParameterDirection.IN).clause( + personView.call(p) + ).build(); + + var assertion = structurallyEqualTo(expected); + assertThrows(AssertionError.class, () -> assertThat(actual, assertion)); + } + + @Test + void clauseCountMismatchTest() { + var p = Variable.of("p"); + var q = Variable.of("q"); + var friend = new Symbol<>("friend", 2, Boolean.class, false); + var friendView = new KeyOnlyView<>(friend); + + var expected = Dnf.builder("Expected") + .parameters(p, q) + .clause(friendView.call(p, q)) + .clause(friendView.call(q, p)) + .build(); + var actual = Dnf.builder("Actual").parameters(p, q).clause( + friendView.call(p, q) + ).build(); + + var assertion = structurallyEqualTo(expected); + assertThrows(AssertionError.class, () -> assertThat(actual, assertion)); + } + + @Test + void literalCountMismatchTest() { + var p = Variable.of("p"); + var q = Variable.of("q"); + var friend = new Symbol<>("friend", 2, Boolean.class, false); + var friendView = new KeyOnlyView<>(friend); + + var expected = Dnf.builder("Expected").parameters(p, q).clause( + friendView.call(p, q), + friendView.call(q, p) + ).build(); + var actual = Dnf.builder("Actual").parameters(p, q).clause( + friendView.call(p, q) + ).build(); + + var assertion = structurallyEqualTo(expected); + assertThrows(AssertionError.class, () -> assertThat(actual, assertion)); + } } diff --git a/subprojects/store-query/src/testFixtures/java/tools/refinery/store/query/tests/MismatchDescribingDnfEqualityChecker.java b/subprojects/store-query/src/testFixtures/java/tools/refinery/store/query/tests/MismatchDescribingDnfEqualityChecker.java index a5b7f85a..6a3301b3 100644 --- a/subprojects/store-query/src/testFixtures/java/tools/refinery/store/query/tests/MismatchDescribingDnfEqualityChecker.java +++ b/subprojects/store-query/src/testFixtures/java/tools/refinery/store/query/tests/MismatchDescribingDnfEqualityChecker.java @@ -6,27 +6,47 @@ package tools.refinery.store.query.tests; import org.hamcrest.Description; +import tools.refinery.store.query.dnf.Dnf; +import tools.refinery.store.query.dnf.SymbolicParameter; import tools.refinery.store.query.equality.DeepDnfEqualityChecker; +import tools.refinery.store.query.literal.Literal; + +import java.util.List; class MismatchDescribingDnfEqualityChecker extends DeepDnfEqualityChecker { private final Description description; - private boolean described; + private boolean raw; + private boolean needsDescription = true; MismatchDescribingDnfEqualityChecker(Description description) { this.description = description; } - public boolean isDescribed() { - return described; + public boolean needsDescription() { + return needsDescription; + } + + @Override + public boolean dnfEqualRaw(List symbolicParameters, List> clauses, Dnf other) { + try { + raw = true; + boolean result = super.dnfEqualRaw(symbolicParameters, clauses, other); + if (!result && needsDescription) { + description.appendText("was ").appendText(other.toDefinitionString()); + } + return false; + } finally { + raw = false; + } } @Override protected boolean doCheckEqual(Pair pair) { boolean result = super.doCheckEqual(pair); - if (!result && !described) { + if (!result && needsDescription) { describeMismatch(pair); // Only describe the first found (innermost) mismatch. - described = true; + needsDescription = false; } return result; } @@ -34,7 +54,7 @@ class MismatchDescribingDnfEqualityChecker extends DeepDnfEqualityChecker { private void describeMismatch(Pair pair) { var inProgress = getInProgress(); int size = inProgress.size(); - if (size <= 1) { + if (size <= 1 && !raw) { description.appendText("was ").appendText(pair.right().toDefinitionString()); return; } diff --git a/subprojects/store-query/src/testFixtures/java/tools/refinery/store/query/tests/QueryMatchers.java b/subprojects/store-query/src/testFixtures/java/tools/refinery/store/query/tests/QueryMatchers.java index 8706ef70..cd449a6a 100644 --- a/subprojects/store-query/src/testFixtures/java/tools/refinery/store/query/tests/QueryMatchers.java +++ b/subprojects/store-query/src/testFixtures/java/tools/refinery/store/query/tests/QueryMatchers.java @@ -7,13 +7,40 @@ package tools.refinery.store.query.tests; import org.hamcrest.Matcher; import tools.refinery.store.query.dnf.Dnf; +import tools.refinery.store.query.dnf.SymbolicParameter; +import tools.refinery.store.query.literal.Literal; + +import java.util.List; public final class QueryMatchers { private QueryMatchers() { throw new IllegalStateException("This is a static utility class and should not be instantiated directly"); } + /** + * Compare two {@link Dnf} instances up to renaming of variables. + * + * @param expected The expected {@link Dnf} instance. + * @return A Hamcrest matcher for equality up to renaming of variables. + */ public static Matcher structurallyEqualTo(Dnf expected) { return new StructurallyEqualTo(expected); } + + /** + * Compare a {@link Dnf} instance to another predicate in DNF form without constructing it. + *

+ * This matcher should be used instead of {@link #structurallyEqualTo(Dnf)} when the validation and + * pre-processing associated with the {@link Dnf} constructor, i.e., validation of parameter directions, + * topological sorting of literals, and the reduction of trivial predicates is not desired. In particular, this + * matcher can be used to test for exact order of literal after pre-processing. + * + * @param expectedSymbolicParameters The expected list of symbolic parameters. + * @param expectedLiterals The expected clauses. Each clause is represented by a list of literals. + * @return A Hamcrest matcher for equality up to renaming of variables. + */ + public static Matcher structurallyEqualTo(List expectedSymbolicParameters, + List> expectedLiterals) { + return new StructurallyEqualToRaw(expectedSymbolicParameters, expectedLiterals); + } } diff --git a/subprojects/store-query/src/testFixtures/java/tools/refinery/store/query/tests/StructurallyEqualTo.java b/subprojects/store-query/src/testFixtures/java/tools/refinery/store/query/tests/StructurallyEqualTo.java index ba51c084..86149141 100644 --- a/subprojects/store-query/src/testFixtures/java/tools/refinery/store/query/tests/StructurallyEqualTo.java +++ b/subprojects/store-query/src/testFixtures/java/tools/refinery/store/query/tests/StructurallyEqualTo.java @@ -29,7 +29,7 @@ public class StructurallyEqualTo extends TypeSafeMatcher { if (describingChecker.dnfEqual(expected, item)) { throw new IllegalStateException("Mismatched Dnf was matching on repeated comparison"); } - if (!describingChecker.isDescribed()) { + if (describingChecker.needsDescription()) { super.describeMismatchSafely(item, mismatchDescription); } } diff --git a/subprojects/store-query/src/testFixtures/java/tools/refinery/store/query/tests/StructurallyEqualToRaw.java b/subprojects/store-query/src/testFixtures/java/tools/refinery/store/query/tests/StructurallyEqualToRaw.java new file mode 100644 index 00000000..2f8c2944 --- /dev/null +++ b/subprojects/store-query/src/testFixtures/java/tools/refinery/store/query/tests/StructurallyEqualToRaw.java @@ -0,0 +1,51 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.store.query.tests; + +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; +import tools.refinery.store.query.dnf.Dnf; +import tools.refinery.store.query.dnf.SymbolicParameter; +import tools.refinery.store.query.equality.DeepDnfEqualityChecker; +import tools.refinery.store.query.literal.Literal; + +import java.util.List; + +public class StructurallyEqualToRaw extends TypeSafeMatcher { + private final List expectedSymbolicParameters; + private final List> expectedClauses; + + public StructurallyEqualToRaw(List expectedSymbolicParameters, + List> expectedClauses) { + this.expectedSymbolicParameters = expectedSymbolicParameters; + this.expectedClauses = expectedClauses; + } + + @Override + protected boolean matchesSafely(Dnf item) { + var checker = new DeepDnfEqualityChecker(); + return checker.dnfEqualRaw(expectedSymbolicParameters, expectedClauses, item); + } + + @Override + protected void describeMismatchSafely(Dnf item, Description mismatchDescription) { + var describingChecker = new MismatchDescribingDnfEqualityChecker(mismatchDescription); + if (describingChecker.dnfEqualRaw(expectedSymbolicParameters, expectedClauses, item)) { + throw new IllegalStateException("Mismatched Dnf was matching on repeated comparison"); + } + if (describingChecker.needsDescription()) { + super.describeMismatchSafely(item, mismatchDescription); + } + } + + @Override + public void describeTo(Description description) { + description.appendText("structurally equal to ") + .appendValueList("(", ", ", ")", expectedSymbolicParameters) + .appendText(" <-> ") + .appendValueList("", ", ", ".", expectedClauses); + } +} -- cgit v1.2.3-54-g00ecf