diff --git a/projects/core/src/testFixtures/java/dan200/computercraft/test/core/StructuralEqualities.java b/projects/core/src/testFixtures/java/dan200/computercraft/test/core/StructuralEqualities.java new file mode 100644 index 000000000..a35a4a04c --- /dev/null +++ b/projects/core/src/testFixtures/java/dan200/computercraft/test/core/StructuralEqualities.java @@ -0,0 +1,130 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.test.core; + +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; + +import javax.annotation.Nullable; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; + +/** + * Concrete implementations for {@link StructuralEquality}. + */ +final class StructuralEqualities { + static final DefaultEquality DEFAULT = new DefaultEquality(); + + private StructuralEqualities() { + } + + static void describeNullable(Description description, StructuralEquality equality, @Nullable T value) { + if (value == null) { + description.appendText("null"); + } else { + equality.describe(description, value); + } + } + + static final class DefaultEquality implements StructuralEquality { + private DefaultEquality() { + } + + @Override + public boolean equals(Object left, Object right) { + return Objects.equals(left, right); + } + + @Override + public void describe(Description description, Object object) { + description.appendValue(object); + } + } + + static final class AllEquality implements StructuralEquality { + private final List> equalities; + + AllEquality(List> equalities) { + this.equalities = equalities; + } + + @Override + public boolean equals(T left, T right) { + return equalities.stream().allMatch(x -> x.equals(left, right)); + } + + @Override + public void describe(Description description, T object) { + description.appendText("{"); + var separator = false; + for (var equality : equalities) { + if (separator) description.appendText(", "); + separator = true; + equality.describe(description, object); + } + description.appendText("}"); + } + } + + static final class FeatureEquality implements StructuralEquality { + private final String desc; + private final Function get; + private final StructuralEquality inner; + + FeatureEquality(String desc, Function get, StructuralEquality inner) { + this.desc = desc; + this.inner = inner; + this.get = get; + } + + private @Nullable U get(T value) { + return get.apply(value); + } + + @Override + public boolean equals(T left, T right) { + var leftInner = get(left); + var rightInner = get(right); + if (leftInner == null) return rightInner == null; + if (rightInner == null) return false; + return inner.equals(leftInner, rightInner); + } + + @Override + public void describe(Description description, T object) { + description.appendText(desc).appendText("=>"); + describeNullable(description, inner, get.apply(object)); + } + } + + static final class EqualityMatcher extends TypeSafeMatcher { + private final StructuralEquality equality; + private final T equalTo; + + EqualityMatcher(Class klass, StructuralEquality equality, T equalTo) { + super(klass); + this.equality = equality; + this.equalTo = equalTo; + } + + @Override + public boolean matchesSafely(T actual) { + if (actual == null) return false; + return equality.equals(actual, equalTo); + } + + @Override + public void describeTo(Description description) { + equality.describe(description, equalTo); + } + + @Override + protected void describeMismatchSafely(T value, Description description) { + description.appendText("was "); + describeNullable(description, equality, value); + } + } +} diff --git a/projects/core/src/testFixtures/java/dan200/computercraft/test/core/StructuralEquality.java b/projects/core/src/testFixtures/java/dan200/computercraft/test/core/StructuralEquality.java new file mode 100644 index 000000000..ac0d26de8 --- /dev/null +++ b/projects/core/src/testFixtures/java/dan200/computercraft/test/core/StructuralEquality.java @@ -0,0 +1,141 @@ +// SPDX-FileCopyrightText: 2023 The CC: Tweaked Developers +// +// SPDX-License-Identifier: MPL-2.0 + +package dan200.computercraft.test.core; + +import org.hamcrest.Description; +import org.hamcrest.Matcher; + +import java.lang.reflect.Field; +import java.util.List; +import java.util.function.Function; + +/** + * A basic mechanism for checking equality of two objects, suitable for use with Hamcrest {@linkplain Matcher matchers}. + *

+ * This is intended for when an object does not override {@link Object#equals(Object)} itself, + * + * @param The type of the value to check. + */ +public interface StructuralEquality { + /** + * Check if two non-null values are equal. + * + * @param left The first value to check. + * @param right The second value to check. + * @return If these values are equal. + */ + boolean equals(T left, T right); + + /** + * Describe this value. + * + * @param description The description to write to. + * @param object The object to describe. + */ + void describe(Description description, T object); + + /** + * Convert this equality instance to a {@link Matcher}. + * + * @param klass The expected type of this object. + * @param expected The expected value. + * @return A matcher which checks if its input is equal to the given value. + */ + default Matcher asMatcher(Class klass, T expected) { + return new StructuralEqualities.EqualityMatcher<>(klass, this, expected); + } + + /** + * The default {@link StructuralEquality} implementation, which just uses {@link Object#equals(Object)}. + * + * @return The default equality. + */ + static StructuralEquality defaultEquality() { + return StructuralEqualities.DEFAULT; + } + + /** + * Checks all equalities match. This is intended for use with {@link #at(String, Function, StructuralEquality)}, to + * check the structure of an object. + * + * @param equalities The equalities which should match. + * @param The type of the object to match. + * @return The newly created {@link StructuralEquality} object. + */ + @SafeVarargs + @SuppressWarnings("varargs") + static StructuralEquality all(StructuralEquality... equalities) { + return new StructuralEqualities.AllEquality<>(List.of(equalities)); + } + + /** + * Create an equality which checks if {@code f(x) = f(y)}, where {@code f} is some projection function (such as + * reading a field). + * + * @param desc The description of this projection. + * @param project The projection function. + * @param inner The inner equality, + * @param The type of the object to check. + * @param The type of the "inner" object, projected out by {@code f} + * @return The newly created {@link StructuralEquality} object. + */ + static StructuralEquality at(String desc, Function project, StructuralEquality inner) { + return new StructuralEqualities.FeatureEquality<>(desc, project, inner); + } + + /** + * A simple version of {@link #at(String, Function, StructuralEquality)} which uses {@link #defaultEquality()} . + * + * @param desc The description of this projection. + * @param project The projection function. + * @param The type of the object to check. + * @param The type of the "inner" object, projected out by {@code f} + * @return The newly created {@link StructuralEquality} object. + */ + static StructuralEquality at(String desc, Function project) { + return at(desc, project, defaultEquality()); + } + + /** + * A hacky version of {@link #at(String, Function, StructuralEquality)} which projects out a private field. + * + * @param klass The class where the field is defined. + * @param fieldName The name of the field. + * @param inner The inner equality. + * @param The type of the object to check. + * @param The type of the field's. + * @return The newly created {@link StructuralEquality} object. + */ + @SuppressWarnings("unchecked") + static StructuralEquality field(Class klass, String fieldName, StructuralEquality inner) { + Field field; + try { + field = klass.getDeclaredField(fieldName); + field.setAccessible(true); + } catch (ReflectiveOperationException e) { + throw new IllegalArgumentException("Cannot find field", e); + } + + return new StructuralEqualities.FeatureEquality<>(fieldName, x -> { + try { + return (U) field.get(x); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException("Cannot read field", e); + } + }, inner); + } + + /** + * A hacky version of {@link #at(String, Function)} which projects out a private field. + * + * @param klass The class where the field is defined. + * @param fieldName The name of the field. + * @param The type of the object to check. + * @return The newly created {@link StructuralEquality} object. + */ + static StructuralEquality field(Class klass, String fieldName) { + return field(klass, fieldName, defaultEquality()); + } +}