Skip to content

Commit

Permalink
Pretty-print floating point numbers in failure messages for `isWithin…
Browse files Browse the repository at this point in the history
…` assertions.

RELNOTES=Pretty-print floating point numbers in failure messages for `isWithin` assertions.
PiperOrigin-RevId: 721848489
  • Loading branch information
kluever authored and Google Java Core Libraries committed Jan 31, 2025
1 parent 31c254d commit de78553
Show file tree
Hide file tree
Showing 8 changed files with 174 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
package com.google.common.truth;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.truth.Fact.fact;
import static com.google.common.truth.Fact.numericFact;
import static com.google.common.truth.Fact.simpleFact;

import java.math.BigDecimal;
Expand Down Expand Up @@ -95,7 +95,10 @@ public void isEquivalentAccordingToCompareTo(@Nullable BigDecimal expected) {

private void compareValues(@Nullable BigDecimal expected) {
if (checkNotNull(actual).compareTo(checkNotNull(expected)) != 0) {
failWithoutActual(fact("expected", expected), butWas(), simpleFact("(scale is ignored)"));
failWithoutActual(
numericFact("expected", expected),
numericFact("but was", actual),
simpleFact("(scale is ignored)"));
}
}
}
15 changes: 7 additions & 8 deletions core/src/main/java/com/google/common/truth/DoubleSubject.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,10 @@

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.truth.Fact.fact;
import static com.google.common.truth.Fact.numericFact;
import static com.google.common.truth.Fact.simpleFact;
import static com.google.common.truth.MathUtil.equalWithinTolerance;
import static com.google.common.truth.MathUtil.notEqualWithinTolerance;
import static com.google.common.truth.Platform.doubleToString;
import static java.lang.Double.NaN;
import static java.lang.Double.doubleToLongBits;

Expand Down Expand Up @@ -115,9 +114,9 @@ public void of(double expected) {

if (!equalWithinTolerance(actual, expected, tolerance)) {
failWithoutActual(
fact("expected", doubleToString(expected)),
butWas(),
fact("outside tolerance", doubleToString(tolerance)));
numericFact("expected", expected),
numericFact("but was", actual),
numericFact("outside tolerance", tolerance));
}
}
};
Expand Down Expand Up @@ -154,9 +153,9 @@ public void of(double expected) {

if (!notEqualWithinTolerance(actual, expected, tolerance)) {
failWithoutActual(
fact("expected not to be", doubleToString(expected)),
butWas(),
fact("within tolerance", doubleToString(tolerance)));
numericFact("expected not to be", expected),
numericFact("but was", actual),
numericFact("within tolerance", tolerance));
}
}
};
Expand Down
76 changes: 60 additions & 16 deletions core/src/main/java/com/google/common/truth/Fact.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,13 @@
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Strings.padEnd;
import static com.google.common.base.Strings.padStart;
import static com.google.common.truth.Platform.doubleToString;
import static com.google.common.truth.Platform.floatToString;
import static java.lang.Math.max;

import com.google.common.collect.ImmutableList;
import java.io.Serializable;
import java.math.BigDecimal;
import org.jspecify.annotations.Nullable;

/**
Expand Down Expand Up @@ -68,29 +71,58 @@ public static Fact simpleFact(String key) {
* Creates a fact with the given key and value, which will be printed in a format like "key:
* value." The numeric value is converted to a string with delimiting commas.
*/
static Fact numericFact(String key, @Nullable Long value) {
static Fact numericFact(String key, @Nullable Number value) {
return new Fact(key, formatNumericValue(value), true);
}

/**
* Creates a fact with the given key and value, which will be printed in a format like "key:
* value." The numeric value is converted to a string with delimiting commas.
* Formats the given numeric value as a string with delimiting commas.
*
* <p><b>Note:</b> only {@code Long}, {@code Integer}, {@code Float}, {@code Double} and {@link
* BigDecimal} are supported.
*/
static Fact numericFact(String key, @Nullable Integer value) {
return new Fact(key, formatNumericValue(value), true);
}

static String formatNumericValue(@Nullable Object value) {
static String formatNumericValue(@Nullable Number value) {
if (value == null) {
return "null";
}
// the value must be a numeric type
checkArgument(
value instanceof Long
|| value instanceof Integer
|| value instanceof Float
|| value instanceof Double
|| value instanceof BigDecimal,
"Value (%s) must be either a Long, Integer, Float, Double, or BigDecimal.",
value);

// DecimalFormat is not available on all platforms, so we do the formatting manually.

if (!(value instanceof BigDecimal) && isInfiniteOrNaN(value.doubleValue())) {
return value.toString();
}
String stringValue =
value instanceof Double
? doubleToString((double) value)
: value instanceof Float //
? floatToString((float) value)
: value.toString();
if (stringValue.contains("E")) {
return stringValue;
}
int decimalIndex = stringValue.indexOf('.');
if (decimalIndex == -1) {
return formatWholeNumericValue(stringValue);
}
String wholeNumbers = stringValue.substring(0, decimalIndex);
String decimal = stringValue.substring(decimalIndex);
return formatWholeNumericValue(wholeNumbers) + decimal;
}

// We only support Long and Integer for now; maybe FP numbers in the future?
checkArgument(value instanceof Long || value instanceof Integer);

// DecimalFormat is not available on all platforms
String stringValue = String.valueOf(value);
private static boolean isInfiniteOrNaN(double d) {
return Double.isInfinite(d) || Double.isNaN(d);
}

private static String formatWholeNumericValue(String stringValue) {
boolean isNegative = stringValue.startsWith("-");
if (isNegative) {
stringValue = stringValue.substring(1);
Expand Down Expand Up @@ -132,13 +164,18 @@ public String toString() {
*/
static String makeMessage(ImmutableList<String> messages, ImmutableList<Fact> facts) {
int longestKeyLength = 0;
int longestValueLength = 0;
int longestIntPartValueLength = 0;
boolean seenNewlineInValue = false;
for (Fact fact : facts) {
if (fact.value != null) {
longestKeyLength = max(longestKeyLength, fact.key.length());
if (fact.padStart) {
longestValueLength = max(longestValueLength, fact.value.length());
int decimalIndex = fact.value.indexOf('.');
if (decimalIndex != -1) {
longestIntPartValueLength = max(longestIntPartValueLength, decimalIndex);
} else {
longestIntPartValueLength = max(longestIntPartValueLength, fact.value.length());
}
}
// TODO(cpovirk): Look for other kinds of newlines.
seenNewlineInValue |= fact.value.contains("\n");
Expand Down Expand Up @@ -172,7 +209,14 @@ static String makeMessage(ImmutableList<String> messages, ImmutableList<Fact> fa
builder.append(padEnd(fact.key, longestKeyLength, ' '));
builder.append(": ");
if (fact.padStart) {
builder.append(padStart(fact.value, longestValueLength, ' '));
int decimalIndex = fact.value.indexOf('.');
if (decimalIndex != -1) {
builder.append(
padStart(fact.value.substring(0, decimalIndex), longestIntPartValueLength, ' '));
builder.append(fact.value.substring(decimalIndex));
} else {
builder.append(padStart(fact.value, longestIntPartValueLength, ' '));
}
} else {
builder.append(fact.value);
}
Expand Down
15 changes: 7 additions & 8 deletions core/src/main/java/com/google/common/truth/FloatSubject.java
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,10 @@

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.truth.Fact.fact;
import static com.google.common.truth.Fact.numericFact;
import static com.google.common.truth.Fact.simpleFact;
import static com.google.common.truth.MathUtil.equalWithinTolerance;
import static com.google.common.truth.MathUtil.notEqualWithinTolerance;
import static com.google.common.truth.Platform.floatToString;
import static java.lang.Float.NaN;
import static java.lang.Float.floatToIntBits;

Expand Down Expand Up @@ -123,9 +122,9 @@ public void of(float expected) {

if (!equalWithinTolerance(actual, expected, tolerance)) {
failWithoutActual(
fact("expected", floatToString(expected)),
butWas(),
fact("outside tolerance", floatToString(tolerance)));
numericFact("expected", expected),
numericFact("but was", actual),
numericFact("outside tolerance", tolerance));
}
}
};
Expand Down Expand Up @@ -162,9 +161,9 @@ public void of(float expected) {

if (!notEqualWithinTolerance(actual, expected, tolerance)) {
failWithoutActual(
fact("expected not to be", floatToString(expected)),
butWas(),
fact("within tolerance", floatToString(tolerance)));
numericFact("expected not to be", expected),
numericFact("but was", actual),
numericFact("within tolerance", tolerance));
}
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,19 @@ public void isEqualToIgnoringScale_string() {
assertFailureValue("but was", "10");
}

@Test
public void isEqualToIgnoringScale_stringWithDecimals() {
BigDecimal tenFour = new BigDecimal("10.4");
assertThat(tenFour).isEqualToIgnoringScale("10.4");
assertThat(tenFour).isEqualToIgnoringScale("10.4");
assertThat(tenFour).isEqualToIgnoringScale("10.40");
assertThat(tenFour).isEqualToIgnoringScale("10.400");
expectFailureWhenTestingThat(tenFour).isEqualToIgnoringScale("3.4");
assertFailureKeys("expected", "but was", "(scale is ignored)");
assertFailureValue("expected", "3.4");
assertFailureValue("but was", "10.4");
}

private BigDecimalSubject expectFailureWhenTestingThat(BigDecimal actual) {
return expectFailure.whenTesting().that(actual);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
package com.google.common.truth;

import static com.google.common.truth.ExpectFailure.assertThat;
import static com.google.common.truth.Platform.doubleToString;
import static com.google.common.truth.Fact.formatNumericValue;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.fail;

Expand Down Expand Up @@ -109,9 +109,9 @@ public void invokeAssertion(SimpleSubjectBuilder<DoubleSubject, Double> expect)
.factKeys()
.containsExactly("expected", "but was", "outside tolerance")
.inOrder();
assertThat(failure).factValue("expected").isEqualTo(doubleToString(expected));
assertThat(failure).factValue("but was").isEqualTo(doubleToString(actual));
assertThat(failure).factValue("outside tolerance").isEqualTo(doubleToString(tolerance));
assertThat(failure).factValue("expected").isEqualTo(formatNumericValue(expected));
assertThat(failure).factValue("but was").isEqualTo(formatNumericValue(actual));
assertThat(failure).factValue("outside tolerance").isEqualTo(formatNumericValue(tolerance));
}

@Test
Expand All @@ -137,8 +137,8 @@ public void invokeAssertion(SimpleSubjectBuilder<DoubleSubject, Double> expect)
}
};
AssertionError failure = expectFailure(callback);
assertThat(failure).factValue("expected not to be").isEqualTo(doubleToString(expected));
assertThat(failure).factValue("within tolerance").isEqualTo(doubleToString(tolerance));
assertThat(failure).factValue("expected not to be").isEqualTo(formatNumericValue(expected));
assertThat(failure).factValue("within tolerance").isEqualTo(formatNumericValue(tolerance));
}

@Test
Expand Down
Loading

0 comments on commit de78553

Please sign in to comment.