Skip to content

Commit

Permalink
Add isWithin().of() support to LongSubject.
Browse files Browse the repository at this point in the history
RELNOTES=Added `isWithin().of()` support to `LongSubject`.
PiperOrigin-RevId: 586097910
  • Loading branch information
java-team-github-bot authored and Google Java Core Libraries committed Nov 28, 2023
1 parent 6376c39 commit 0e99a27
Show file tree
Hide file tree
Showing 3 changed files with 265 additions and 0 deletions.
109 changes: 109 additions & 0 deletions core/src/main/java/com/google/common/truth/LongSubject.java
Expand Up @@ -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;

/**
Expand All @@ -25,12 +30,111 @@
* @author Kurt Alfred Kluever
*/
public class LongSubject extends ComparableSubject<Long> {

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
* <i>or</i> if it was expected <i>not</i> 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)));
}
}
};
}

/**
Expand All @@ -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}.
*
Expand Down
18 changes: 18 additions & 0 deletions core/src/main/java/com/google/common/truth/MathUtil.java
Expand Up @@ -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
Expand Down
138 changes: 138 additions & 0 deletions core/src/test/java/com/google/common/truth/LongSubjectTest.java
Expand Up @@ -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;
Expand Down Expand Up @@ -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<LongSubject, Long> callback =
new ExpectFailure.SimpleSubjectBuilderCallback<LongSubject, Long>() {
@Override
public void invokeAssertion(SimpleSubjectBuilder<LongSubject, Long> 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<LongSubject, Long> callback =
new ExpectFailure.SimpleSubjectBuilderCallback<LongSubject, Long>() {
@Override
public void invokeAssertion(SimpleSubjectBuilder<LongSubject, Long> 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<LongSubject, Long> LONG_SUBJECT_FACTORY =
new Subject.Factory<LongSubject, Long>() {
@Override
public LongSubject createSubject(FailureMetadata metadata, Long that) {
return new LongSubject(metadata, that);
}
};

@CanIgnoreReturnValue
private static AssertionError expectFailure(
SimpleSubjectBuilderCallback<LongSubject, Long> callback) {
return ExpectFailure.expectFailureAbout(LONG_SUBJECT_FACTORY, callback);
}

private LongSubject expectFailureWhenTestingThat(Long actual) {
return expectFailure.whenTesting().that(actual);
}
Expand Down

0 comments on commit 0e99a27

Please sign in to comment.