• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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