1 /* 2 * Copyright (c) 2017 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.extensions.proto; 18 19 /** 20 * A generic entity in the {@link DiffResult} tree with queryable properties. 21 * 22 * <p>This class is not directly extensible. Only the inner classes, {@link 23 * RecursableDiffEntity.WithoutResultCode} and {@link RecursableDiffEntity.WithResultCode}, can be 24 * extended. 25 * 26 * <p>A {@code RecursableDiffEntity}'s base properties (i.e., {@link #isMatched()}, {@link 27 * #isIgnored()}) are determined differently depending on the subtype. The {@code WithoutResultCode} 28 * subtype derives its base properties entirely from its children, while the {@code WithResultCode} 29 * subtype derives its base properties from an enum explicitly set on the entity. The {@link 30 * #isAnyChildMatched()} and {@link #isAnyChildIgnored()} properties are determined recursively on 31 * both subtypes. 32 * 33 * <p>A {@code RecursableDiffEntity} may have no children. The nature and count of an entity's 34 * children depends on the implementation - see {@link DiffResult} for concrete instances. 35 */ 36 abstract class RecursableDiffEntity { 37 38 // Lazily-initialized return values for the recursive properties of the entity. 39 // null = not initialized yet 40 // 41 // This essentially implements what @Memoized does, but @AutoValue doesn't support @Memoized on 42 // parent classes. I think it's better to roll-our-own in the parent class to take advantage of 43 // inheritance, than to duplicate the @Memoized methods for every subclass. 44 45 private Boolean isAnyChildIgnored = null; 46 private Boolean isAnyChildMatched = null; 47 48 // Only extended by inner classes. RecursableDiffEntity()49 private RecursableDiffEntity() {} 50 51 /** 52 * The children of this entity. May be empty. 53 * 54 * <p>Subclasses should {@link @Memoized} this method especially if it's expensive. 55 */ childEntities()56 abstract Iterable<? extends RecursableDiffEntity> childEntities(); 57 58 /** Returns whether or not the two entities matched according to the diff rules. */ isMatched()59 abstract boolean isMatched(); 60 61 /** Returns true if all sub-fields of both entities were ignored for comparison. */ isIgnored()62 abstract boolean isIgnored(); 63 64 /** 65 * Returns true if some child entity matched. 66 * 67 * <p>Caches the result for future calls. 68 */ isAnyChildMatched()69 final boolean isAnyChildMatched() { 70 if (isAnyChildMatched == null) { 71 isAnyChildMatched = false; 72 for (RecursableDiffEntity entity : childEntities()) { 73 if ((entity.isMatched() && !entity.isContentEmpty()) || entity.isAnyChildMatched()) { 74 isAnyChildMatched = true; 75 break; 76 } 77 } 78 } 79 return isAnyChildMatched; 80 } 81 82 /** 83 * Returns true if some child entity was ignored. 84 * 85 * <p>Caches the result for future calls. 86 */ isAnyChildIgnored()87 final boolean isAnyChildIgnored() { 88 if (isAnyChildIgnored == null) { 89 isAnyChildIgnored = false; 90 for (RecursableDiffEntity entity : childEntities()) { 91 if ((entity.isIgnored() && !entity.isContentEmpty()) || entity.isAnyChildIgnored()) { 92 isAnyChildIgnored = true; 93 break; 94 } 95 } 96 } 97 return isAnyChildIgnored; 98 } 99 100 /** 101 * Prints the contents of this diff entity to {@code sb}. 102 * 103 * @param includeMatches Whether to include reports for fields which matched. 104 * @param fieldPrefix The human-readable field path leading to this entity. Empty if this is the 105 * root entity. 106 * @param sb Builder to print the text to. 107 */ printContents(boolean includeMatches, String fieldPrefix, StringBuilder sb)108 abstract void printContents(boolean includeMatches, String fieldPrefix, StringBuilder sb); 109 110 /** Returns true if this entity has no contents to print, with or without includeMatches. */ isContentEmpty()111 abstract boolean isContentEmpty(); 112 printChildContents(boolean includeMatches, String fieldPrefix, StringBuilder sb)113 final void printChildContents(boolean includeMatches, String fieldPrefix, StringBuilder sb) { 114 for (RecursableDiffEntity entity : childEntities()) { 115 entity.printContents(includeMatches, fieldPrefix, sb); 116 } 117 } 118 119 /** 120 * A generic entity in the {@link DiffResult} tree without a result code. 121 * 122 * <p>This entity derives its {@code isMatched()} and {@code isIgnored()} state purely from its 123 * children. If it has no children, it is considered both matched and ignored. 124 */ 125 abstract static class WithoutResultCode extends RecursableDiffEntity { 126 127 private Boolean isMatched = null; 128 private Boolean isIgnored = null; 129 130 @Override isMatched()131 final boolean isMatched() { 132 if (isMatched == null) { 133 isMatched = true; 134 for (RecursableDiffEntity entity : childEntities()) { 135 if (!entity.isMatched()) { 136 isMatched = false; 137 break; 138 } 139 } 140 } 141 return isMatched; 142 } 143 144 @Override isIgnored()145 final boolean isIgnored() { 146 if (isIgnored == null) { 147 isIgnored = true; 148 for (RecursableDiffEntity entity : childEntities()) { 149 if (!entity.isIgnored()) { 150 isIgnored = false; 151 break; 152 } 153 } 154 } 155 return isIgnored; 156 } 157 } 158 159 /** 160 * A generic entity in the {@link DiffResult} tree with a result code. 161 * 162 * <p>The result code overrides {@code isMatched()} and {@code isIgnored()} evaluation, using the 163 * provided enum instead of any child states. 164 */ 165 abstract static class WithResultCode extends RecursableDiffEntity { 166 enum Result { 167 /** No differences. The expected case. */ 168 MATCHED, 169 170 /** expected() didn't have this field, actual() did. */ 171 ADDED, 172 173 /** actual() didn't have this field, expected() did. */ 174 REMOVED, 175 176 /** Both messages had the field but the values don't match. */ 177 MODIFIED, 178 179 /** 180 * The message was moved from one index to another, but strict ordering was expected. 181 * 182 * <p>This is only possible on {@link DiffResult.RepeatedField.PairResult}. 183 */ 184 MOVED_OUT_OF_ORDER, 185 186 /** 187 * The messages were ignored for the sake of comparison. 188 * 189 * <p>IGNORED fields should also be considered MATCHED, for the sake of pass/fail decisions. 190 * The IGNORED information is useful for limiting diff output: i.e., if all fields in a deep 191 * submessage-to-submessage comparison are ignored, we can print the top-level type as ignored 192 * and omit diff lines for the rest of the fields within. 193 */ 194 IGNORED; 195 builder()196 static Builder builder() { 197 return new Builder(); 198 } 199 200 /** 201 * A helper class for computing a {@link Result}. It defaults to {@code MATCHED}, but can be 202 * changed exactly once if called with a true {@code condition}. 203 * 204 * <p>All subsequent 'mark' calls after a successful mark are ignored. 205 */ 206 static final class Builder { 207 private Result state = Result.MATCHED; 208 Builder()209 private Builder() {} 210 markAddedIf(boolean condition)211 public void markAddedIf(boolean condition) { 212 setIf(condition, Result.ADDED); 213 } 214 markRemovedIf(boolean condition)215 public void markRemovedIf(boolean condition) { 216 setIf(condition, Result.REMOVED); 217 } 218 markModifiedIf(boolean condition)219 public void markModifiedIf(boolean condition) { 220 setIf(condition, Result.MODIFIED); 221 } 222 build()223 public Result build() { 224 return state; 225 } 226 setIf(boolean condition, Result newState)227 private void setIf(boolean condition, Result newState) { 228 if (condition && state == Result.MATCHED) { 229 state = newState; 230 } 231 } 232 } 233 } 234 result()235 abstract Result result(); 236 237 @Override isMatched()238 final boolean isMatched() { 239 return result() == Result.MATCHED || result() == Result.IGNORED; 240 } 241 242 @Override isIgnored()243 final boolean isIgnored() { 244 return result() == Result.IGNORED; 245 } 246 } 247 } 248