blob: 301ad19dd6dca54b9233adf8e4a84e592054be3d [file] [log] [blame]
/*
* Copyright (c) 2011 Google, Inc.
*
* 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 com.google.common.truth;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.truth.StringUtil.format;
import static com.google.common.truth.SubjectUtils.accumulate;
import com.google.common.base.Objects;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.util.List;
import javax.annotation.Nullable;
/**
* A {@code Subject} in {@code Truth} fits into the assertion fluent chain, taking the place
* of the "subject of the test". For instance, in {@code assertThat("foo").isNotNull()},
* {@code Truth#assertThat(String)} returns a StringSubject which wraps the actual value itself,
* providing a hook for methods which test propositions about the actual value.
*
* <p>Custom sub-types of Subject provide type-appropriate methods which can then provide more
* suitable error messages than the traditional assertions may provide.
*
* @param <S> the self-type, allowing {@code this}-returning methods to avoid needing subclassing
* @param <T> the type of the object being tested by this {@code Subject}
* @author David Saff
* @author Christian Gruber
*/
public class Subject<S extends Subject<S, T>, T> {
protected final FailureStrategy failureStrategy;
private final T actual;
private String customName = null;
public Subject(FailureStrategy failureStrategy, @Nullable T actual) {
this.failureStrategy = checkNotNull(failureStrategy);
this.actual = actual;
}
/** An internal method used to obtain the value set by {@link #named(String, Object...)}. */
protected String internalCustomName() {
return customName;
}
/**
* Adds a prefix to the subject, when it is displayed in error messages. This is especially useful
* in the context of types that have no helpful {@code toString()} representation, e.g. boolean.
* Writing {@code assertThat(foo).named("foo").isTrue();} then results in a more reasonable error
* message.
*
* <p>{@code named()} takes a format template and argument objects which will be substituted into
* the template, similar to {@link String#format(String, Object...)}, the chief difference being
* that extra parameters (for which there are no template variables) will be appended to the
* resulting string in brackets. Additionally, this only supports the {@code %s} template variable
* type.
*/
@SuppressWarnings("unchecked")
@CanIgnoreReturnValue
public S named(String format, Object... args) {
checkNotNull(format, "Name passed to named() cannot be null.");
this.customName = StringUtil.format(format, args);
return (S) this;
}
/** Fails if the subject is not null. */
public void isNull() {
if (actual() != null) {
fail("is null");
}
}
/** Fails if the subject is null. */
public void isNotNull() {
if (actual() == null) {
failWithoutActual("is a non-null reference");
}
}
/**
* Fails if the subject is not equal to the given object. For the purposes of this comparison, two
* objects are equal if any of the following is true:
*
* <ul>
* <li>they are equal according to {@link Objects#equal}
* <li>they are arrays and are considered equal by the appropriate {@link Arrays#equals} overload
* <li>they are boxed integer types ({@code Byte}, {@code Short}, {@code Character}, {@code
* Integer}, or {@code Long}) and they are numerically equal when converted to {@code Long}.
* </ul>
*/
public void isEqualTo(@Nullable Object other) {
doEqualCheck(actual(), other, true);
}
/**
* Fails if the subject is equal to the given object. The meaning of equality is the same as for
* the {@link #isEqualTo} method.
*/
public void isNotEqualTo(@Nullable Object other) {
doEqualCheck(actual(), other, false);
}
private void doEqualCheck(
@Nullable Object rawSubject, @Nullable Object rawOther, boolean expectEqual) {
Object subject;
Object other;
if (isIntegralBoxedPrimitive(rawSubject) && isIntegralBoxedPrimitive(rawOther)) {
subject = integralValue(rawSubject);
other = integralValue(rawOther);
} else {
subject = rawSubject;
other = rawOther;
}
if (Objects.equal(subject, other) != expectEqual) {
failComparingToStrings(
expectEqual ? "is equal to" : "is not equal to", subject, other, rawOther, expectEqual);
}
}
private static boolean isIntegralBoxedPrimitive(@Nullable Object o) {
return o instanceof Byte
|| o instanceof Short
|| o instanceof Character
|| o instanceof Integer
|| o instanceof Long;
}
private static Long integralValue(Object o) {
if (o instanceof Character) {
return (long) ((Character) o).charValue();
} else if (o instanceof Number) {
return ((Number) o).longValue();
} else {
throw new AssertionError(o + " must be either a Character or a Number.");
}
}
/** Fails if the subject is not the same instance as the given object. */
public void isSameAs(@Nullable Object other) {
if (actual() != other) {
failComparingToStrings("is the same instance as", actual(), other, other, true);
}
}
/** Fails if the subject is the same instance as the given object. */
public void isNotSameAs(@Nullable Object other) {
if (actual() == other) {
fail("is not the same instance as", other);
}
}
/** Fails if the subject is not an instance of the given class. */
public void isInstanceOf(Class<?> clazz) {
if (clazz == null) {
throw new NullPointerException("clazz");
}
if (!Platform.isInstanceOfType(actual(), clazz)) {
if (actual() != null) {
failWithBadResults(
"is an instance of",
clazz.getName(),
"is an instance of",
actual().getClass().getName());
} else {
fail("is an instance of", clazz.getName());
}
}
}
/** Fails if the subject is an instance of the given class. */
public void isNotInstanceOf(Class<?> clazz) {
if (clazz == null) {
throw new NullPointerException("clazz");
}
if (actual() == null) {
return; // null is not an instance of clazz.
}
if (Platform.isInstanceOfType(actual(), clazz)) {
failWithRawMessage(
"%s expected not to be an instance of %s, but was.",
actualAsString(), clazz.getName());
}
}
/** Fails unless the subject is equal to any element in the given iterable. */
public void isIn(Iterable<?> iterable) {
if (!Iterables.contains(iterable, actual())) {
fail("is equal to any element in", iterable);
}
}
/** Fails unless the subject is equal to any of the given elements. */
public void isAnyOf(@Nullable Object first, @Nullable Object second, @Nullable Object... rest) {
List<Object> list = accumulate(first, second, rest);
if (!list.contains(actual())) {
fail("is equal to any of", list);
}
}
/** Fails if the subject is equal to any element in the given iterable. */
public void isNotIn(Iterable<?> iterable) {
int index = Iterables.indexOf(iterable, Predicates.<Object>equalTo(actual()));
if (index != -1) {
failWithRawMessage(
"Not true that %s is not in %s. It was found at index %s",
actualAsString(), iterable, index);
}
}
/** Fails if the subject is equal to any of the given elements. */
public void isNoneOf(@Nullable Object first, @Nullable Object second, @Nullable Object... rest) {
isNotIn(accumulate(first, second, rest));
}
/**
* @deprecated Prefer {@code #actual()} for direct access to the subject.
*/
@Deprecated
protected T getSubject() {
// TODO(cgruber): move functionality to actual() and delete when no callers.
return actual;
}
/**
* Returns the unedited, unformatted raw actual value.
*/
protected final T actual() {
return getSubject();
}
/**
* @deprecated Prefer {@code #actualAsString()} for display-formatted access to the subject.
*/
@Deprecated
protected String getDisplaySubject() {
// TODO(cgruber) migrate people from this method once no one is subclassing it.
String formatted = actualCustomStringRepresentation();
if (customName != null) {
// Covers some rare cases where a type might return "" from their custom formatter.
// This is actually pretty terrible, as it comes from subjects overriding (formerly)
// getDisplaySubject() in cases of .named() to make it not prefixing but replacing.
// That goes against the stated contract of .named(). Once displayedAs() is in place,
// we can rip this out and callers can use that instead.
// TODO(cgruber)
return customName + (formatted.isEmpty() ? "" : " (<" + formatted + ">)");
} else {
return "<" + formatted + ">";
}
}
/**
* Returns a string representation of the actual value. This will either be the toString() of
* the value or a prefixed "name" along with the string representation.
*/
protected final String actualAsString() {
return getDisplaySubject();
}
/**
* Supplies the direct string representation of the actual value to other methods which may
* prefix or otherwise position it in an error message. This should only be overridden to
* provide an improved string representation of the value under test, as it would appear in
* any given error message, and should not be used for additional prefixing.
*
* <p>Subjects should override this with care.
*
* <p>By default, this returns {@code String.ValueOf(getActualValue())}.
*/
protected String actualCustomStringRepresentation() {
return String.valueOf(actual());
}
/**
* A convenience for implementers of {@link Subject} subclasses to use other truth {@code Subject}
* wrappers within their own propositional logic.
*/
protected TestVerb check() {
return new TestVerb(failureStrategy);
}
/**
* Reports a failure constructing a message from a simple verb.
*
* @param proposition the proposition being asserted
*/
protected final void fail(String proposition) {
failureStrategy.fail("Not true that " + actualAsString() + " " + proposition);
}
/**
* Assembles a failure message and passes such to the FailureStrategy. Also performs
* disambiguation if the subject and {@code other} have the same toString()'s.
*
* @param verb the proposition being asserted
* @param other the value against which the subject is compared
*/
protected final void fail(String verb, Object other) {
failComparingToStrings(verb, actual(), other, other, false);
}
private void failComparingToStrings(
String verb, Object subject, Object other, Object displayOther, boolean compareToStrings) {
StringBuilder message =
new StringBuilder("Not true that ").append(actualAsString()).append(" ");
// If the subject and parts aren't null, and they have equal toString()'s but different
// classes, we need to disambiguate them.
boolean neitherNull = (other != null) && (subject != null);
boolean sameToStrings = actualCustomStringRepresentation().equals(String.valueOf(other));
boolean needsClassDisambiguation =
neitherNull && sameToStrings && !subject.getClass().equals(other.getClass());
if (needsClassDisambiguation) {
message.append("(").append(subject.getClass().getName()).append(") ");
}
message.append(verb).append(" <").append(displayOther).append(">");
if (needsClassDisambiguation) {
message.append(" (").append(other.getClass().getName()).append(")");
}
if (!needsClassDisambiguation && sameToStrings && compareToStrings) {
message.append(" (although their toString() representations are the same)");
}
failureStrategy.fail(message.toString());
}
/**
* Assembles a failure message and passes such to the FailureStrategy
*
* @param verb the proposition being asserted
* @param messageParts the expectations against which the subject is compared
*/
protected final void fail(String verb, Object... messageParts) {
// For backwards binary compatibility
if (messageParts.length == 0) {
fail(verb);
} else if (messageParts.length == 1) {
fail(verb, messageParts[0]);
} else {
StringBuilder message = new StringBuilder("Not true that ");
message.append(actualAsString()).append(" ").append(verb);
for (Object part : messageParts) {
message.append(" <").append(part).append(">");
}
failureStrategy.fail(message.toString());
}
}
/**
* Assembles a failure message and passes it to the FailureStrategy
*
* @param verb the proposition being asserted
* @param expected the expectations against which the subject is compared
* @param failVerb the failure of the proposition being asserted
* @param actual the actual value the subject was compared against
*/
protected final void failWithBadResults(
String verb, Object expected, String failVerb, Object actual) {
String message =
format(
"Not true that %s %s <%s>. It %s <%s>",
actualAsString(),
verb,
expected,
failVerb,
(actual == null) ? "null reference" : actual);
failureStrategy.fail(message);
}
/**
* Assembles a failure message with an alternative representation of the wrapped subject and
* passes it to the FailureStrategy
*
* @param verb the proposition being asserted
* @param expected the expected value of the proposition
* @param actual the custom representation of the subject to be reported in the failure.
*/
protected final void failWithCustomSubject(String verb, Object expected, Object actual) {
String message =
format(
"Not true that <%s> %s <%s>",
(actual == null) ? "null reference" : actual, verb, expected);
failureStrategy.fail(message);
}
/** @deprecated Use {@link #failWithoutActual(String)} */
@Deprecated
protected final void failWithoutSubject(String proposition) {
String strSubject = this.customName == null ? "the subject" : "\"" + customName + "\"";
failureStrategy.fail(format("Not true that %s %s", strSubject, proposition));
}
/**
* Assembles a failure message without a given subject and passes it to the FailureStrategy
*
* @param proposition the proposition being asserted
*/
protected final void failWithoutActual(String proposition) {
failWithoutSubject(proposition);
}
/**
* Passes through a failure message verbatim. Used for {@link Subject} subclasses which need to
* provide alternate language for more fit-to-purpose error messages.
*
* @param message the message template to be passed to the failure. Note, this method only
* guarantees to process {@code %s} tokens. It is not guaranteed to be compatible with {@code
* String.format()}. Any other formatting desired (such as floats or scientific notation)
* should be performed before the method call and the formatted value passed in as a string.
* @param parameters the object parameters which will be applied to the message template.
*/
// TODO(cgruber) final
protected void failWithRawMessage(String message, Object... parameters) {
failureStrategy.fail(format(message, parameters));
}
/**
* @throws UnsupportedOperationException always
* @deprecated {@link Object#equals(Object)} is not supported on Truth subjects. If you meant to
* test object equality between an expected and the actual value, use
* {@link #isEqualTo(Object)} instead.
*/
@Deprecated
@Override
public final boolean equals(@Nullable Object o) {
throw new UnsupportedOperationException(
"If you meant to test object equality, use .isEqualTo(other) instead.");
}
/**
* @throws UnsupportedOperationException always
* @deprecated {@link Object#hashCode()} is not supported on Truth subjects.
*/
@Deprecated
@Override
public final int hashCode() {
throw new UnsupportedOperationException("Subject.hashCode() is not supported.");
}
}