1 /* 2 * Copyright (c) 2018 Google, Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.google.common.truth; 18 19 import static com.google.common.base.Preconditions.checkNotNull; 20 import static com.google.common.base.Strings.padEnd; 21 import static java.lang.Math.max; 22 23 import com.google.common.collect.ImmutableList; 24 import java.io.Serializable; 25 import org.jspecify.annotations.Nullable; 26 27 /** 28 * A string key-value pair in a failure message, such as "expected: abc" or "but was: xyz." 29 * 30 * <p>Most Truth users will never interact with this type. It appears in the Truth API only as a 31 * parameter to methods like {@link Subject#failWithActual(Fact, Fact...)}, which are used only by 32 * custom {@code Subject} implementations. 33 * 34 * <p>If you are writing a custom {@code Subject}, see <a 35 * href="https://truth.dev/failure_messages">our tips on writing failure messages</a>. 36 */ 37 public final class Fact implements Serializable { 38 /** 39 * Creates a fact with the given key and value, which will be printed in a format like "key: 40 * value." The value is converted to a string by calling {@code String.valueOf} on it. 41 */ fact(String key, @Nullable Object value)42 public static Fact fact(String key, @Nullable Object value) { 43 return new Fact(key, String.valueOf(value)); 44 } 45 46 /** 47 * Creates a fact with no value, which will be printed in the format "key" (with no colon or 48 * value). 49 * 50 * <p>In most cases, prefer {@linkplain #fact key-value facts}, which give Truth more flexibility 51 * in how to format the fact for display. {@code simpleFact} is useful primarily for: 52 * 53 * <ul> 54 * <li>messages from no-arg assertions. For example, {@code isNotEmpty()} would generate the 55 * fact "expected not to be empty" 56 * <li>prose that is part of a larger message. For example, {@code contains()} sometimes 57 * displays facts like "expected to contain: ..." <i>"but did not"</i> "though it did 58 * contain: ..." 59 * </ul> 60 */ simpleFact(String key)61 public static Fact simpleFact(String key) { 62 return new Fact(key, null); 63 } 64 65 final String key; 66 final @Nullable String value; 67 Fact(String key, @Nullable String value)68 private Fact(String key, @Nullable String value) { 69 this.key = checkNotNull(key); 70 this.value = value; 71 } 72 73 /** 74 * Returns a simple string representation for the fact. While this is used in the output of {@code 75 * TruthFailureSubject}, it's not used in normal failure messages, which automatically align facts 76 * horizontally and indent multiline values. 77 */ 78 @Override toString()79 public String toString() { 80 return value == null ? key : key + ": " + value; 81 } 82 83 /** 84 * Formats the given messages and facts into a string for use as the message of a test failure. In 85 * particular, this method horizontally aligns the beginning of fact values. 86 */ makeMessage(ImmutableList<String> messages, ImmutableList<Fact> facts)87 static String makeMessage(ImmutableList<String> messages, ImmutableList<Fact> facts) { 88 int longestKeyLength = 0; 89 boolean seenNewlineInValue = false; 90 for (Fact fact : facts) { 91 if (fact.value != null) { 92 longestKeyLength = max(longestKeyLength, fact.key.length()); 93 // TODO(cpovirk): Look for other kinds of newlines. 94 seenNewlineInValue |= fact.value.contains("\n"); 95 } 96 } 97 98 StringBuilder builder = new StringBuilder(); 99 for (String message : messages) { 100 builder.append(message); 101 builder.append('\n'); 102 } 103 104 /* 105 * *Usually* the first fact is printed at the beginning of a new line. However, when this 106 * exception is the cause of another exception, that exception will print it starting after 107 * "Caused by: " on the same line. The other exception sometimes also reuses this message as its 108 * own message. In both of those scenarios, the first line doesn't start at column 0, so the 109 * horizontal alignment is thrown off. 110 * 111 * There's not much we can do about this, short of always starting with a newline (which would 112 * leave a blank line at the beginning of the message in the normal case). 113 */ 114 for (Fact fact : facts) { 115 if (fact.value == null) { 116 builder.append(fact.key); 117 } else if (seenNewlineInValue) { 118 builder.append(fact.key); 119 builder.append(":\n"); 120 builder.append(indent(fact.value)); 121 } else { 122 builder.append(padEnd(fact.key, longestKeyLength, ' ')); 123 builder.append(": "); 124 builder.append(fact.value); 125 } 126 builder.append('\n'); 127 } 128 if (builder.length() > 0) { 129 builder.setLength(builder.length() - 1); // remove trailing \n 130 } 131 return builder.toString(); 132 } 133 indent(String value)134 private static String indent(String value) { 135 // We don't want to indent with \t because the text would align exactly with the stack trace. 136 // We don't want to indent with \t\t because it would be very far for people with 8-space tabs. 137 // Let's compromise and indent by 4 spaces, which is different than both 2- and 8-space tabs. 138 return " " + value.replace("\n", "\n "); 139 } 140 } 141