1 /* 2 * Copyright (c) 2014 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.Strings.commonPrefix; 20 import static com.google.common.base.Strings.commonSuffix; 21 import static com.google.common.truth.Fact.fact; 22 import static com.google.common.truth.SubjectUtils.concat; 23 import static java.lang.Character.isHighSurrogate; 24 import static java.lang.Character.isLowSurrogate; 25 import static java.lang.Math.max; 26 27 import com.google.common.annotations.VisibleForTesting; 28 import com.google.common.collect.ImmutableList; 29 import org.jspecify.annotations.Nullable; 30 31 /** 32 * Contains part of the code responsible for creating a JUnit {@code ComparisonFailure} (if 33 * available) or a plain {@code AssertionError} (if not). 34 * 35 * <p>This particular class is responsible for the fallback when a platform offers {@code 36 * ComparisonFailure} but it is not available in a particular test environment. In practice, that 37 * should mean open-source JRE users who choose to exclude our JUnit 4 dependency. 38 * 39 * <p>(This class also includes logic to format expected and actual values for easier reading.) 40 * 41 * <p>Another part of the fallback logic is {@code Platform.ComparisonFailureWithFacts}, which has a 42 * different implementation under GWT/j2cl, where {@code ComparisonFailure} is also unavailable but 43 * we can't just recover from that at runtime. 44 */ 45 final class ComparisonFailures { makeComparisonFailureFacts( ImmutableList<Fact> headFacts, ImmutableList<Fact> tailFacts, String expected, String actual)46 static ImmutableList<Fact> makeComparisonFailureFacts( 47 ImmutableList<Fact> headFacts, 48 ImmutableList<Fact> tailFacts, 49 String expected, 50 String actual) { 51 return concat(headFacts, formatExpectedAndActual(expected, actual), tailFacts); 52 } 53 54 /** 55 * Returns one or more facts describing the difference between the given expected and actual 56 * values. 57 * 58 * <p>Currently, that means either 2 facts (one each for expected and actual) or 1 fact with a 59 * diff-like (but much simpler) view. 60 * 61 * <p>In the case of 2 facts, the facts contain either the full expected and actual values or, if 62 * the values have a long prefix or suffix in common, abbreviated values with "…" at the beginning 63 * or end. 64 */ 65 @VisibleForTesting formatExpectedAndActual(String expected, String actual)66 static ImmutableList<Fact> formatExpectedAndActual(String expected, String actual) { 67 ImmutableList<Fact> result; 68 69 // TODO(cpovirk): Call attention to differences in trailing whitespace. 70 // TODO(cpovirk): And changes in the *kind* of whitespace characters in the middle of the line. 71 72 result = Platform.makeDiff(expected, actual); 73 if (result != null) { 74 return result; 75 } 76 77 result = removeCommonPrefixAndSuffix(expected, actual); 78 if (result != null) { 79 return result; 80 } 81 82 return ImmutableList.of(fact("expected", expected), fact("but was", actual)); 83 } 84 removeCommonPrefixAndSuffix( String expected, String actual)85 private static @Nullable ImmutableList<Fact> removeCommonPrefixAndSuffix( 86 String expected, String actual) { 87 int originalExpectedLength = expected.length(); 88 89 // TODO(cpovirk): Use something like BreakIterator where available. 90 /* 91 * TODO(cpovirk): If the abbreviated values contain newlines, maybe expand them to contain a 92 * newline on each end so that we don't start mid-line? That way, horizontally aligned text will 93 * remain horizontally aligned. But of course, for many multi-line strings, we won't enter this 94 * method at all because we'll generate diff-style output instead. So we might not need to worry 95 * too much about newlines here. 96 */ 97 // TODO(cpovirk): Avoid splitting in the middle of "\r\n." 98 int prefix = commonPrefix(expected, actual).length(); 99 prefix = max(0, prefix - CONTEXT); 100 while (prefix > 0 && validSurrogatePairAt(expected, prefix - 1)) { 101 prefix--; 102 } 103 // No need to hide the prefix unless it's long. 104 if (prefix > 3) { 105 expected = "…" + expected.substring(prefix); 106 actual = "…" + actual.substring(prefix); 107 } 108 109 int suffix = commonSuffix(expected, actual).length(); 110 suffix = max(0, suffix - CONTEXT); 111 while (suffix > 0 && validSurrogatePairAt(expected, expected.length() - suffix - 1)) { 112 suffix--; 113 } 114 // No need to hide the suffix unless it's long. 115 if (suffix > 3) { 116 expected = expected.substring(0, expected.length() - suffix) + "…"; 117 actual = actual.substring(0, actual.length() - suffix) + "…"; 118 } 119 120 if (originalExpectedLength - expected.length() < WORTH_HIDING) { 121 return null; 122 } 123 124 return ImmutableList.of(fact("expected", expected), fact("but was", actual)); 125 } 126 127 private static final int CONTEXT = 20; 128 private static final int WORTH_HIDING = 60; 129 130 // From c.g.c.base.Strings. validSurrogatePairAt(CharSequence string, int index)131 private static boolean validSurrogatePairAt(CharSequence string, int index) { 132 return index >= 0 133 && index <= (string.length() - 2) 134 && isHighSurrogate(string.charAt(index)) 135 && isLowSurrogate(string.charAt(index + 1)); 136 } 137 ComparisonFailures()138 private ComparisonFailures() {} 139 } 140