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