From 1cecba7b00ea8f0b8f714759434b658ca4a92de8 Mon Sep 17 00:00:00 2001 From: Marten Gajda Date: Sun, 20 Aug 2023 10:14:19 +0200 Subject: [PATCH] Add Quality of a properly implemented Equals Contract, closes #16 --- .../quality/iterable/IteratesInAnyOrder.java | 4 +- .../confidence/quality/object/EqualTo.java | 16 ++- .../quality/object/HashCodeEquals.java | 42 ++++++++ .../quality/object/StrictlyEqualTo.java | 52 +++++++++ .../quality/object/HashCodeEqualsTest.java | 52 +++++++++ .../quality/object/StrictlyEqualToTest.java | 101 ++++++++++++++++++ 6 files changed, 256 insertions(+), 11 deletions(-) create mode 100644 confidence-core/src/main/java/org/saynotobugs/confidence/quality/object/HashCodeEquals.java create mode 100644 confidence-core/src/main/java/org/saynotobugs/confidence/quality/object/StrictlyEqualTo.java create mode 100644 confidence-core/src/test/java/org/saynotobugs/confidence/quality/object/HashCodeEqualsTest.java create mode 100644 confidence-core/src/test/java/org/saynotobugs/confidence/quality/object/StrictlyEqualToTest.java diff --git a/confidence-core/src/main/java/org/saynotobugs/confidence/quality/iterable/IteratesInAnyOrder.java b/confidence-core/src/main/java/org/saynotobugs/confidence/quality/iterable/IteratesInAnyOrder.java index 3a46d32..99789f7 100644 --- a/confidence-core/src/main/java/org/saynotobugs/confidence/quality/iterable/IteratesInAnyOrder.java +++ b/confidence-core/src/main/java/org/saynotobugs/confidence/quality/iterable/IteratesInAnyOrder.java @@ -146,7 +146,7 @@ private static Assessment resolve( { for (int j = 0; j < qualities.size(); j++) { - if (asses(candidates.get(i), qualities.get((j)), cache)) + if (assess(candidates.get(i), qualities.get((j)), cache)) { List can2 = new ArrayList<>(candidates); can2.remove(i); @@ -165,7 +165,7 @@ private static Assessment resolve( @SuppressWarnings("NewApi") - private static Boolean asses(T candidate, Quality quality, Map, Boolean>> cache) + private static Boolean assess(T candidate, Quality quality, Map, Boolean>> cache) { return cache.computeIfAbsent(candidate, i -> new HashMap<>()) .computeIfAbsent(quality, i -> quality.assessmentOf(candidate).isSuccess()); diff --git a/confidence-core/src/main/java/org/saynotobugs/confidence/quality/object/EqualTo.java b/confidence-core/src/main/java/org/saynotobugs/confidence/quality/object/EqualTo.java index 32320b7..e7b186f 100644 --- a/confidence-core/src/main/java/org/saynotobugs/confidence/quality/object/EqualTo.java +++ b/confidence-core/src/main/java/org/saynotobugs/confidence/quality/object/EqualTo.java @@ -20,7 +20,6 @@ import org.dmfs.jems2.iterable.Mapped; import org.dmfs.srcless.annotations.staticfactory.StaticFactories; -import org.saynotobugs.confidence.Assessment; import org.saynotobugs.confidence.Quality; import org.saynotobugs.confidence.assessment.FailPrepended; import org.saynotobugs.confidence.assessment.PassIf; @@ -39,16 +38,15 @@ public final class EqualTo extends QualityComposition */ public EqualTo(T expected) { - super(actual -> asses(expected, actual), + super(actual -> expected.getClass().isArray() && actual.getClass().isArray() + ? + new FailPrepended( + new Text("array that"), + new Iterates<>(new Mapped<>(EqualTo::new, new ArrayIterable(expected))).assessmentOf(new ArrayIterable(actual))) + : + new PassIf(expected.equals(actual), new Value(actual)), new Value(expected)); } - private static Assessment asses(T expected, T actual) - { - return expected.getClass().isArray() && actual.getClass().isArray() - ? new FailPrepended(new Text("array that"), - new Iterates<>(new Mapped<>(EqualTo::new, new ArrayIterable(expected))).assessmentOf(new ArrayIterable(actual))) - : new PassIf(expected.equals(actual), new Value(actual)); - } } diff --git a/confidence-core/src/main/java/org/saynotobugs/confidence/quality/object/HashCodeEquals.java b/confidence-core/src/main/java/org/saynotobugs/confidence/quality/object/HashCodeEquals.java new file mode 100644 index 0000000..89a86de --- /dev/null +++ b/confidence-core/src/main/java/org/saynotobugs/confidence/quality/object/HashCodeEquals.java @@ -0,0 +1,42 @@ +/* + * Copyright 2023 dmfs GmbH + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.saynotobugs.confidence.quality.object; + +import org.saynotobugs.confidence.Description; +import org.saynotobugs.confidence.description.Spaced; +import org.saynotobugs.confidence.description.Text; +import org.saynotobugs.confidence.description.Value; +import org.saynotobugs.confidence.quality.composite.Has; +import org.saynotobugs.confidence.quality.composite.QualityComposition; + +public final class HashCodeEquals extends QualityComposition +{ + + public HashCodeEquals(int hashCode) + { + super(new Has<>("hashCode", Object::hashCode, new EqualTo<>(hashCode))); + } + + public HashCodeEquals(Object referenceObject) + { + super(new Has<>((Description orig) -> new Spaced(new Text("has hashCode"), orig, new Text("like"), new Value(referenceObject)), + orig -> new Spaced(new Text("had hashCode"), orig), + Object::hashCode, new EqualTo<>(referenceObject.hashCode()))); + } +} diff --git a/confidence-core/src/main/java/org/saynotobugs/confidence/quality/object/StrictlyEqualTo.java b/confidence-core/src/main/java/org/saynotobugs/confidence/quality/object/StrictlyEqualTo.java new file mode 100644 index 0000000..2d3a919 --- /dev/null +++ b/confidence-core/src/main/java/org/saynotobugs/confidence/quality/object/StrictlyEqualTo.java @@ -0,0 +1,52 @@ +/* + * Copyright 2022 dmfs GmbH + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.saynotobugs.confidence.quality.object; + +import org.dmfs.srcless.annotations.staticfactory.StaticFactories; +import org.saynotobugs.confidence.Quality; +import org.saynotobugs.confidence.description.Spaced; +import org.saynotobugs.confidence.description.Text; +import org.saynotobugs.confidence.description.Value; +import org.saynotobugs.confidence.quality.composite.AllOf; +import org.saynotobugs.confidence.quality.composite.QualityComposition; +import org.saynotobugs.confidence.quality.grammar.Is; + + +@StaticFactories(value = "Core", packageName = "org.saynotobugs.confidence.quality") +public final class StrictlyEqualTo extends QualityComposition +{ + /** + * A {@link Quality} that is satisfied if the value under test is strictly equal to the given value. + *

+ * "Strictly equal" means the object satisfies the equals contract as described in {@link Object#equals(Object)}. + * This is intended to test implementation of the equals contract, not for general testing for equality and may result + * in unexpected outcomes, especially when used with negation. + *

+ * Note, this {@link Quality} does not support arrays. + */ + public StrictlyEqualTo(T expected) + { + super(new AllOf<>( + new Is<>(new Satisfies<>(actual -> actual.equals(actual), any -> new Text("not reflexive"), new Text("reflexive"))), + new Is<>(new Satisfies<>(actual -> !actual.equals(null), any -> new Text("equal to null"), new Text("not equal to null"))), + new Is<>(new Satisfies<>(expected::equals, any -> new Spaced(new Text("not equal to"), new Value(expected)), new Spaced(new Text("equal to"), new Value(expected)))), + new Is<>(new Satisfies<>(actual -> actual.equals(expected), any -> new Text("not symmetric"), new Text("symmetric"))), + new HashCodeEquals(expected))); + } +} diff --git a/confidence-core/src/test/java/org/saynotobugs/confidence/quality/object/HashCodeEqualsTest.java b/confidence-core/src/test/java/org/saynotobugs/confidence/quality/object/HashCodeEqualsTest.java new file mode 100644 index 0000000..87dc51f --- /dev/null +++ b/confidence-core/src/test/java/org/saynotobugs/confidence/quality/object/HashCodeEqualsTest.java @@ -0,0 +1,52 @@ +/* + * Copyright 2023 dmfs GmbH + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.saynotobugs.confidence.quality.object; + +import org.junit.jupiter.api.Test; +import org.saynotobugs.confidence.quality.composite.AllOf; +import org.saynotobugs.confidence.test.quality.Fails; +import org.saynotobugs.confidence.test.quality.HasDescription; +import org.saynotobugs.confidence.test.quality.Passes; + +import static org.saynotobugs.confidence.Assertion.assertThat; + +class HashCodeEqualsTest +{ + @Test + void testHashCode() + { + assertThat(new HashCodeEquals(123), + new AllOf<>( + new Passes<>(123, 123L), + new Fails<>(124, "had hashCode <124>"), + new HasDescription("has hashCode <123>") + )); + } + + @Test + void testReferenceObject() + { + assertThat(new HashCodeEquals("123"), + new AllOf<>( + new Passes<>("123"), + new Fails(321,"had hashCode <321>"), + new HasDescription("has hashCode <48690> like \"123\"") + )); + } +} \ No newline at end of file diff --git a/confidence-core/src/test/java/org/saynotobugs/confidence/quality/object/StrictlyEqualToTest.java b/confidence-core/src/test/java/org/saynotobugs/confidence/quality/object/StrictlyEqualToTest.java new file mode 100644 index 0000000..f9b2645 --- /dev/null +++ b/confidence-core/src/test/java/org/saynotobugs/confidence/quality/object/StrictlyEqualToTest.java @@ -0,0 +1,101 @@ +/* + * Copyright 2023 dmfs GmbH + * + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.saynotobugs.confidence.quality.object; + +import org.junit.jupiter.api.Test; +import org.saynotobugs.confidence.test.quality.Fails; +import org.saynotobugs.confidence.test.quality.HasDescription; +import org.saynotobugs.confidence.test.quality.Passes; + +import static org.saynotobugs.confidence.Assertion.assertThat; +import static org.saynotobugs.confidence.quality.Core.allOf; + +class StrictlyEqualToTest +{ + @Test + void testSingle() + { + assertThat(new StrictlyEqualTo<>("123"), + allOf( + new Passes<>("123"), + new Fails("456", "{ ...\n was not equal to \"123\"\n and\n was not symmetric\n and\n had hashCode <51669> }"), + new Fails<>(new Object() + { + @Override + public boolean equals(Object obj) + { + return "123".equals(obj); + } + + @Override + public int hashCode() + { + return "123".hashCode(); + } + + @Override + public String toString() + { + return "fakeObject1"; + } + }, "{ was not reflexive\n ...\n was not equal to \"123\"\n ... }"), + new Fails<>(new Object() + { + @Override + public boolean equals(Object obj) + { + return "123".equals(obj) || obj == null; + } + + @Override + public int hashCode() + { + return "456".hashCode(); + } + + @Override + public String toString() + { + return "fakeObject2"; + } + }, "{ was not reflexive\n and\n was equal to null\n and\n was not equal to \"123\"\n ...\n had hashCode <51669> }"), + new Fails<>(new Object() + { + @Override + public boolean equals(Object obj) + { + return "123".equals(obj) || obj == this; + } + + @Override + public int hashCode() + { + return "123".hashCode(); + } + + @Override + public String toString() + { + return "fakeObject3"; + } + }, "{ ...\n was not equal to \"123\"\n ... }"), + new HasDescription("is reflexive\n and\n is not equal to null\n and\n is equal to \"123\"\n and\n is symmetric\n and\n has hashCode <48690> like \"123\"") + )); + } +} \ No newline at end of file