diff --git a/core/src/main/java/com/google/common/truth/LongSubject.java b/core/src/main/java/com/google/common/truth/LongSubject.java index d56de24d3..4985bf725 100644 --- a/core/src/main/java/com/google/common/truth/LongSubject.java +++ b/core/src/main/java/com/google/common/truth/LongSubject.java @@ -15,6 +15,11 @@ */ package com.google.common.truth; +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.truth.MathUtil.equalWithinTolerance; +import static com.google.common.truth.Fact.fact; + import org.checkerframework.checker.nullness.qual.Nullable; /** @@ -25,12 +30,111 @@ * @author Kurt Alfred Kluever */ public class LongSubject extends ComparableSubject { + + private final @Nullable Long actual; + /** * Constructor for use by subclasses. If you want to create an instance of this class itself, call * {@link Subject#check(String, Object...) check(...)}{@code .that(actual)}. */ protected LongSubject(FailureMetadata metadata, @Nullable Long actual) { super(metadata, actual); + this.actual = actual; + } + + /** + * A partially specified check about an approximate relationship to a {@code long} subject using a + * tolerance. + * + * @since 1.2 + */ + public abstract static class TolerantLongComparison { + + // Prevent subclassing outside of this class + private TolerantLongComparison() {} + + /** + * Fails if the subject was expected to be within the tolerance of the given value but was not + * or if it was expected not to be within the tolerance but was. The subject and + * tolerance are specified earlier in the fluent call chain. + */ + public abstract void of(long expectedLong); + + /** + * @throws UnsupportedOperationException always + * @deprecated {@link Object#equals(Object)} is not supported on TolerantLongComparison. If you + * meant to compare longs, use {@link #of(long)} instead. + */ + @Deprecated + @Override + public boolean equals(@Nullable Object o) { + throw new UnsupportedOperationException( + "If you meant to compare longs, use .of(long) instead."); + } + + /** + * @throws UnsupportedOperationException always + * @deprecated {@link Object#hashCode()} is not supported on TolerantLongComparison + */ + @Deprecated + @Override + public int hashCode() { + throw new UnsupportedOperationException("Subject.hashCode() is not supported."); + } + } + + /** + * Prepares for a check that the subject is a number within the given tolerance of an expected + * value that will be provided in the next call in the fluent chain. + * + * @param tolerance an inclusive upper bound on the difference between the subject and object + * allowed by the check, which must be a non-negative value. + * @since 1.2 + */ + public TolerantLongComparison isWithin(long tolerance) { + return new TolerantLongComparison() { + @Override + public void of(long expected) { + Long actual = LongSubject.this.actual; + checkNotNull( + actual, "actual value cannot be null. tolerance=%s expected=%s", tolerance, expected); + checkTolerance(tolerance); + + if (!equalWithinTolerance(actual, expected, tolerance)) { + failWithoutActual( + fact("expected", Long.toString(expected)), + butWas(), + fact("outside tolerance", Long.toString(tolerance))); + } + } + }; + } + + /** + * Prepares for a check that the subject is a number not within the given tolerance of an expected + * value that will be provided in the next call in the fluent chain. + * + * @param tolerance an exclusive lower bound on the difference between the subject and object + * allowed by the check, which must be a non-negative value. + * @since 1.2 + */ + public TolerantLongComparison isNotWithin(long tolerance) { + return new TolerantLongComparison() { + @Override + public void of(long expected) { + Long actual = LongSubject.this.actual; + checkNotNull( + actual, "actual value cannot be null. tolerance=%s expected=%s", tolerance, expected); + checkTolerance(tolerance); + + if (equalWithinTolerance(actual, expected, tolerance)) { + failWithoutActual( + fact("expected not to be", Long.toString(expected)), + butWas(), + fact("within tolerance", Long.toString(tolerance))); + } + } + }; } /** @@ -42,6 +146,11 @@ public final void isEquivalentAccordingToCompareTo(@Nullable Long other) { super.isEquivalentAccordingToCompareTo(other); } + /** Ensures that the given tolerance is a non-negative value. */ + static void checkTolerance(long tolerance) { + checkArgument(tolerance >= 0, "tolerance (%s) cannot be negative", tolerance); + } + /** * Checks that the subject is greater than {@code other}. * diff --git a/core/src/main/java/com/google/common/truth/MathUtil.java b/core/src/main/java/com/google/common/truth/MathUtil.java index 791ac4d70..c10e85625 100644 --- a/core/src/main/java/com/google/common/truth/MathUtil.java +++ b/core/src/main/java/com/google/common/truth/MathUtil.java @@ -16,12 +16,30 @@ package com.google.common.truth; +import static java.lang.Math.subtractExact; + import com.google.common.primitives.Doubles; /** Math utilities to be shared by numeric subjects. */ final class MathUtil { private MathUtil() {} + /** + * Returns true iff {@code left} and {@code right} are values within {@code tolerance} of each + * other. + */ + /* package */ static boolean equalWithinTolerance(long left, long right, long tolerance) { + try { + // subtractExact is always desugared. + @SuppressWarnings({"AndroidJdkLibsChecker", "Java7ApiChecker"}) + long absDiff = Math.abs(subtractExact(left, right)); + return 0 <= absDiff && absDiff <= Math.abs(tolerance); + } catch (ArithmeticException e) { + // The numbers are so far apart their difference isn't even a long. + return false; + } + } + /** * Returns true iff {@code left} and {@code right} are finite values within {@code tolerance} of * each other. Note that both this method and {@link #notEqualWithinTolerance} returns false if diff --git a/core/src/test/java/com/google/common/truth/LongSubjectTest.java b/core/src/test/java/com/google/common/truth/LongSubjectTest.java index b3a3f4439..63a71a373 100644 --- a/core/src/test/java/com/google/common/truth/LongSubjectTest.java +++ b/core/src/test/java/com/google/common/truth/LongSubjectTest.java @@ -15,8 +15,12 @@ */ package com.google.common.truth; +import static com.google.common.truth.ExpectFailure.assertThat; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.fail; +import com.google.common.truth.ExpectFailure.SimpleSubjectBuilderCallback; +import com.google.errorprone.annotations.CanIgnoreReturnValue; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -129,6 +133,140 @@ public void isAtMost_int() { assertThat(2L).isAtMost(3); } + @Test + public void isWithinOf() { + assertThat(20000L).isWithin(0L).of(20000L); + assertThat(20000L).isWithin(1L).of(20000L); + assertThat(20000L).isWithin(10000L).of(20000L); + assertThat(20000L).isWithin(10000L).of(30000L); + assertThat(Long.MIN_VALUE).isWithin(1L).of(Long.MIN_VALUE + 1); + assertThat(Long.MAX_VALUE).isWithin(1L).of(Long.MAX_VALUE - 1); + assertThat(Long.MAX_VALUE / 2).isWithin(Long.MAX_VALUE).of(-Long.MAX_VALUE / 2); + assertThat(-Long.MAX_VALUE / 2).isWithin(Long.MAX_VALUE).of(Long.MAX_VALUE / 2); + + assertThatIsWithinFails(20000L, 9999L, 30000L); + assertThatIsWithinFails(20000L, 10000L, 30001L); + assertThatIsWithinFails(Long.MIN_VALUE, 0L, Long.MAX_VALUE); + assertThatIsWithinFails(Long.MAX_VALUE, 0L, Long.MIN_VALUE); + assertThatIsWithinFails(Long.MIN_VALUE, 1L, Long.MIN_VALUE + 2); + assertThatIsWithinFails(Long.MAX_VALUE, 1L, Long.MAX_VALUE - 2); + // Don't fall for rollover + assertThatIsWithinFails(Long.MIN_VALUE, 1L, Long.MAX_VALUE); + assertThatIsWithinFails(Long.MAX_VALUE, 1L, Long.MIN_VALUE); + } + + private static void assertThatIsWithinFails(long actual, long tolerance, long expected) { + ExpectFailure.SimpleSubjectBuilderCallback callback = + new ExpectFailure.SimpleSubjectBuilderCallback() { + @Override + public void invokeAssertion(SimpleSubjectBuilder expect) { + expect.that(actual).isWithin(tolerance).of(expected); + } + }; + AssertionError failure = expectFailure(callback); + assertThat(failure) + .factKeys() + .containsExactly("expected", "but was", "outside tolerance") + .inOrder(); + assertThat(failure).factValue("expected").isEqualTo(Long.toString(expected)); + assertThat(failure).factValue("but was").isEqualTo(Long.toString(actual)); + assertThat(failure).factValue("outside tolerance").isEqualTo(Long.toString(tolerance)); + } + + @Test + public void isNotWithinOf() { + assertThatIsNotWithinFails(20000L, 0L, 20000L); + assertThatIsNotWithinFails(20000L, 1L, 20000L); + assertThatIsNotWithinFails(20000L, 10000L, 20000L); + assertThatIsNotWithinFails(20000L, 10000L, 30000L); + assertThatIsNotWithinFails(Long.MIN_VALUE, 1L, Long.MIN_VALUE + 1); + assertThatIsNotWithinFails(Long.MAX_VALUE, 1L, Long.MAX_VALUE - 1); + assertThatIsNotWithinFails(Long.MAX_VALUE / 2, Long.MAX_VALUE, -Long.MAX_VALUE / 2); + assertThatIsNotWithinFails(-Long.MAX_VALUE / 2, Long.MAX_VALUE, Long.MAX_VALUE / 2); + + assertThat(20000L).isNotWithin(9999L).of(30000L); + assertThat(20000L).isNotWithin(10000L).of(30001L); + assertThat(Long.MIN_VALUE).isNotWithin(0L).of(Long.MAX_VALUE); + assertThat(Long.MAX_VALUE).isNotWithin(0L).of(Long.MIN_VALUE); + assertThat(Long.MIN_VALUE).isNotWithin(1L).of(Long.MIN_VALUE + 2); + assertThat(Long.MAX_VALUE).isNotWithin(1L).of(Long.MAX_VALUE - 2); + // Don't fall for rollover + assertThat(Long.MIN_VALUE).isNotWithin(1L).of(Long.MAX_VALUE); + assertThat(Long.MAX_VALUE).isNotWithin(1L).of(Long.MIN_VALUE); + } + + private static void assertThatIsNotWithinFails(long actual, long tolerance, long expected) { + ExpectFailure.SimpleSubjectBuilderCallback callback = + new ExpectFailure.SimpleSubjectBuilderCallback() { + @Override + public void invokeAssertion(SimpleSubjectBuilder expect) { + expect.that(actual).isNotWithin(tolerance).of(expected); + } + }; + AssertionError failure = expectFailure(callback); + assertThat(failure).factValue("expected not to be").isEqualTo(Long.toString(expected)); + assertThat(failure).factValue("within tolerance").isEqualTo(Long.toString(tolerance)); + } + + @Test + public void isWithinIntegers() { + assertThat(20000L).isWithin(0).of(20000); + assertThat(20000L).isWithin(1).of(20000); + assertThat(20000L).isWithin(10000).of(20000); + assertThat(20000L).isWithin(10000).of(30000); + + assertThat(20000L).isNotWithin(0).of(200000); + assertThat(20000L).isNotWithin(1).of(200000); + assertThat(20000L).isNotWithin(10000).of(200000); + assertThat(20000L).isNotWithin(10000).of(300000); + } + + @Test + public void isWithinNegativeTolerance() { + isWithinNegativeToleranceThrowsIAE(0L, -10, 5); + isWithinNegativeToleranceThrowsIAE(0L, -10, 20); + isNotWithinNegativeToleranceThrowsIAE(0L, -10, 5); + isNotWithinNegativeToleranceThrowsIAE(0L, -10, 20); + } + + private static void isWithinNegativeToleranceThrowsIAE( + long actual, long tolerance, long expected) { + try { + assertThat(actual).isWithin(tolerance).of(expected); + fail("Expected IllegalArgumentException to be thrown but wasn't"); + } catch (IllegalArgumentException iae) { + assertThat(iae) + .hasMessageThat() + .isEqualTo("tolerance (" + tolerance + ") cannot be negative"); + } + } + + private static void isNotWithinNegativeToleranceThrowsIAE( + long actual, long tolerance, long expected) { + try { + assertThat(actual).isNotWithin(tolerance).of(expected); + fail("Expected IllegalArgumentException to be thrown but wasn't"); + } catch (IllegalArgumentException iae) { + assertThat(iae) + .hasMessageThat() + .isEqualTo("tolerance (" + tolerance + ") cannot be negative"); + } + } + + private static final Subject.Factory LONG_SUBJECT_FACTORY = + new Subject.Factory() { + @Override + public LongSubject createSubject(FailureMetadata metadata, Long that) { + return new LongSubject(metadata, that); + } + }; + + @CanIgnoreReturnValue + private static AssertionError expectFailure( + SimpleSubjectBuilderCallback callback) { + return ExpectFailure.expectFailureAbout(LONG_SUBJECT_FACTORY, callback); + } + private LongSubject expectFailureWhenTestingThat(Long actual) { return expectFailure.whenTesting().that(actual); }