Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
org.curioswitch.common.testing.assertj.proto.ProtoTruthMessageDifferencer Maven / Gradle / Ivy
Go to download
An assertj extension for fine-grained assertions on protobuf messages.
/*
* MIT License
*
* Copyright (c) 2018 Choko ([email protected] )
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/*
* Copyright (c) 2017 Google, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.curioswitch.common.testing.assertj.proto;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static org.assertj.core.api.Assertions.assertThat;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.protobuf.Descriptors.Descriptor;
import com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.Descriptors.FieldDescriptor.JavaType;
import com.google.protobuf.Message;
import com.google.protobuf.TextFormat;
import com.google.protobuf.UnknownFieldSet;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.Collections;
import java.util.Deque;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.assertj.core.data.Offset;
import org.checkerframework.checker.nullness.compatqual.NullableDecl;
import org.curioswitch.common.testing.assertj.proto.DiffResult.RepeatedField;
import org.curioswitch.common.testing.assertj.proto.DiffResult.SingularField;
import org.curioswitch.common.testing.assertj.proto.DiffResult.UnknownFieldSetDiff;
import org.curioswitch.common.testing.assertj.proto.RecursableDiffEntity.WithResultCode.Result;
/**
* Tool to differentiate two messages with the same {@link Descriptor}, subject to the rules set out
* in a {@link FluentEqualityConfig}.
*
* A {@code ProtoTruthMessageDifferencer} is immutable and thread-safe. Its outputs, however,
* have caching behaviors and are not thread-safe.
*/
final class ProtoTruthMessageDifferencer {
private final FluentEqualityConfig rootConfig;
private final Descriptor rootDescriptor;
private ProtoTruthMessageDifferencer(FluentEqualityConfig rootConfig, Descriptor descriptor) {
rootConfig.validate(descriptor, FieldDescriptorValidator.ALLOW_ALL);
this.rootConfig = rootConfig;
this.rootDescriptor = descriptor;
}
/** Create a new {@link ProtoTruthMessageDifferencer} for the given config and descriptor. */
static ProtoTruthMessageDifferencer create(
FluentEqualityConfig rootConfig, Descriptor descriptor) {
return new ProtoTruthMessageDifferencer(rootConfig, descriptor);
}
/** Compare the two non-null messages, and return a detailed comparison report. */
DiffResult diffMessages(Message actual, Message expected) {
checkNotNull(actual);
checkNotNull(expected);
checkArgument(
actual.getDescriptorForType() == expected.getDescriptorForType(),
"The actual [%s] and expected [%s] message descriptors do not match.",
actual.getDescriptorForType(),
expected.getDescriptorForType());
return diffMessages(actual, expected, rootConfig);
}
private DiffResult diffMessages(Message actual, Message expected, FluentEqualityConfig config) {
DiffResult.Builder builder = DiffResult.newBuilder().setActual(actual).setExpected(expected);
// Compare known fields.
Map actualFields = actual.getAllFields();
Map expectedFields = expected.getAllFields();
for (FieldDescriptor fieldDescriptor :
Sets.union(actualFields.keySet(), expectedFields.keySet())) {
// Check if we should ignore this field. If the result is nonrecursive, proceed anyway, but
// the field will be considered ignored in the final diff report if no sub-fields get compared
// (i.e., the sub-DiffResult winds up empty). This allows us support FieldScopeLogic
// disjunctions without repeating recursive work.
FieldDescriptorOrUnknown fieldDescriptorOrUnknown =
FieldDescriptorOrUnknown.fromFieldDescriptor(fieldDescriptor);
FieldScopeResult shouldCompare =
config.compareFieldsScope().policyFor(rootDescriptor, fieldDescriptorOrUnknown);
if (shouldCompare == FieldScopeResult.EXCLUDED_RECURSIVELY) {
builder.addSingularField(
fieldDescriptor.getNumber(), SingularField.ignored(name(fieldDescriptor)));
continue;
}
if (fieldDescriptor.isRepeated()) {
if (fieldDescriptor.isMapField()) {
Map actualMap = toProtoMap(actualFields.get(fieldDescriptor));
Map expectedMap = toProtoMap(expectedFields.get(fieldDescriptor));
ImmutableSet keyOrder =
Sets.union(actualMap.keySet(), expectedMap.keySet()).immutableCopy();
builder.addAllSingularFields(
fieldDescriptor.getNumber(),
compareMapFieldsByKey(
actualMap,
expectedMap,
keyOrder,
fieldDescriptor,
config.subScope(rootDescriptor, fieldDescriptorOrUnknown)));
} else {
List> actualList = toProtoList(actualFields.get(fieldDescriptor));
List> expectedList = toProtoList(expectedFields.get(fieldDescriptor));
boolean ignoreRepeatedFieldOrder =
config
.ignoreRepeatedFieldOrderScope()
.contains(rootDescriptor, fieldDescriptorOrUnknown);
boolean ignoreExtraRepeatedFieldElements =
config
.ignoreExtraRepeatedFieldElementsScope()
.contains(rootDescriptor, fieldDescriptorOrUnknown);
if (ignoreRepeatedFieldOrder) {
builder.addRepeatedField(
fieldDescriptor.getNumber(),
compareRepeatedFieldIgnoringOrder(
actualList,
expectedList,
shouldCompare == FieldScopeResult.EXCLUDED_NONRECURSIVELY,
fieldDescriptor,
ignoreExtraRepeatedFieldElements,
config.subScope(rootDescriptor, fieldDescriptorOrUnknown)));
} else if (ignoreExtraRepeatedFieldElements && !expectedList.isEmpty()) {
builder.addRepeatedField(
fieldDescriptor.getNumber(),
compareRepeatedFieldExpectingSubsequence(
actualList,
expectedList,
shouldCompare == FieldScopeResult.EXCLUDED_NONRECURSIVELY,
fieldDescriptor,
config.subScope(rootDescriptor, fieldDescriptorOrUnknown)));
} else {
builder.addAllSingularFields(
fieldDescriptor.getNumber(),
compareRepeatedFieldByIndices(
actualList,
expectedList,
shouldCompare == FieldScopeResult.EXCLUDED_NONRECURSIVELY,
fieldDescriptor,
config.subScope(rootDescriptor, fieldDescriptorOrUnknown)));
}
}
} else {
builder.addSingularField(
fieldDescriptor.getNumber(),
compareSingularValue(
actualFields.get(fieldDescriptor),
expectedFields.get(fieldDescriptor),
actual.getDefaultInstanceForType().getField(fieldDescriptor),
shouldCompare == FieldScopeResult.EXCLUDED_NONRECURSIVELY,
fieldDescriptor,
name(fieldDescriptor),
config.subScope(rootDescriptor, fieldDescriptorOrUnknown)));
}
}
// Compare unknown fields.
if (!config.ignoreFieldAbsenceScope().isAll()) {
UnknownFieldSetDiff diff =
diffUnknowns(actual.getUnknownFields(), expected.getUnknownFields(), config);
builder.setUnknownFields(diff);
}
return builder.build();
}
// Helper which takes a proto map in List form, and converts it to a Map
// by extracting the keys and values from the generated map-entry submessages. Returns an empty
// map if null is passed in.
private static Map toProtoMap(@NullableDecl Object container) {
if (container == null) {
return Collections.emptyMap();
}
List> entryMessages = (List>) container;
Map retVal = Maps.newHashMap();
for (Object entry : entryMessages) {
Message message = (Message) entry;
Object key = message.getAllFields().get(message.getDescriptorForType().findFieldByNumber(1));
Object value =
message.getAllFields().get(message.getDescriptorForType().findFieldByNumber(2));
retVal.put(key, value);
}
return retVal;
}
// Takes a List or null, and returns the casted list in the first case, an empty list in
// the latter case.
private static List> toProtoList(@NullableDecl Object container) {
if (container == null) {
return Collections.emptyList();
}
return (List>) container;
}
private List compareMapFieldsByKey(
Map actualMap,
Map expectedMap,
Set keyOrder,
FieldDescriptor mapFieldDescriptor,
FluentEqualityConfig mapConfig) {
FieldDescriptor keyFieldDescriptor = mapFieldDescriptor.getMessageType().findFieldByNumber(1);
FieldDescriptor valueFieldDescriptor = mapFieldDescriptor.getMessageType().findFieldByNumber(2);
FieldDescriptorOrUnknown valueFieldDescriptorOrUnknown =
FieldDescriptorOrUnknown.fromFieldDescriptor(valueFieldDescriptor);
// We never ignore the key, no matter what the logic dictates.
FieldScopeResult compareValues =
mapConfig.compareFieldsScope().policyFor(rootDescriptor, valueFieldDescriptorOrUnknown);
if (compareValues == FieldScopeResult.EXCLUDED_RECURSIVELY) {
return ImmutableList.of(SingularField.ignored(name(mapFieldDescriptor)));
}
boolean ignoreExtraRepeatedFieldElements =
mapConfig
.ignoreExtraRepeatedFieldElementsScope()
.contains(
rootDescriptor, FieldDescriptorOrUnknown.fromFieldDescriptor(mapFieldDescriptor));
FluentEqualityConfig valuesConfig =
mapConfig.subScope(rootDescriptor, valueFieldDescriptorOrUnknown);
ImmutableList.Builder builder =
ImmutableList.builderWithExpectedSize(keyOrder.size());
for (Object key : keyOrder) {
@NullableDecl Object actualValue = actualMap.get(key);
@NullableDecl Object expectedValue = expectedMap.get(key);
if (ignoreExtraRepeatedFieldElements && !expectedMap.isEmpty() && expectedValue == null) {
builder.add(
SingularField.ignored(indexedName(mapFieldDescriptor, key, keyFieldDescriptor)));
} else {
builder.add(
compareSingularValue(
actualValue,
expectedValue,
/*defaultValue=*/ null,
compareValues == FieldScopeResult.EXCLUDED_NONRECURSIVELY,
valueFieldDescriptor,
indexedName(mapFieldDescriptor, key, keyFieldDescriptor),
valuesConfig));
}
}
return builder.build();
}
private RepeatedField compareRepeatedFieldIgnoringOrder(
List> actualList,
List> expectedList,
boolean excludeNonRecursive,
FieldDescriptor fieldDescriptor,
boolean ignoreExtraRepeatedFieldElements,
FluentEqualityConfig config) {
RepeatedField.Builder builder =
RepeatedField.newBuilder()
.setFieldDescriptor(fieldDescriptor)
.setActual(actualList)
.setExpected(expectedList);
// TODO(user): Use maximum bipartite matching here, instead of greedy matching.
Set unmatchedActual = setForRange(actualList.size());
Set unmatchedExpected = setForRange(expectedList.size());
for (int i = 0; i < actualList.size(); i++) {
Object actual = actualList.get(i);
for (int j : unmatchedExpected) {
Object expected = expectedList.get(j);
RepeatedField.PairResult pairResult =
compareRepeatedFieldElementPair(
actual, expected, excludeNonRecursive, fieldDescriptor, i, j, config);
if (pairResult.isMatched()) {
// Found a match - remove both these elements from the candidate pools.
builder.addPairResult(pairResult);
unmatchedActual.remove(i);
unmatchedExpected.remove(j);
break;
}
}
}
// Record remaining unmatched elements.
for (int i : unmatchedActual) {
if (ignoreExtraRepeatedFieldElements && !expectedList.isEmpty()) {
builder.addPairResult(
RepeatedField.PairResult.newBuilder()
.setResult(Result.IGNORED)
.setActual(actualList.get(i))
.setActualFieldIndex(i)
.setFieldDescriptor(fieldDescriptor)
.build());
} else {
builder.addPairResult(
compareRepeatedFieldElementPair(
actualList.get(i),
/*expected=*/ null,
excludeNonRecursive,
fieldDescriptor,
i,
/*expectedFieldIndex=*/ null,
config));
}
}
for (int j : unmatchedExpected) {
builder.addPairResult(
compareRepeatedFieldElementPair(
/*actual=*/ null,
expectedList.get(j),
excludeNonRecursive,
fieldDescriptor,
/*actualFieldIndex=*/ null,
j,
config));
}
return builder.build();
}
private RepeatedField compareRepeatedFieldExpectingSubsequence(
List> actualList,
List> expectedList,
boolean excludeNonRecursive,
FieldDescriptor fieldDescriptor,
FluentEqualityConfig config) {
RepeatedField.Builder builder =
RepeatedField.newBuilder()
.setFieldDescriptor(fieldDescriptor)
.setActual(actualList)
.setExpected(expectedList);
// Search for expectedList as a subsequence of actualList.
//
// This mostly replicates the algorithm used by IterableSubject.containsAll().inOrder(), but
// with some tweaks for fuzzy equality and structured output.
Deque actualIndices = new ArrayDeque<>();
for (int i = 0; i < actualList.size(); i++) {
actualIndices.addLast(i);
}
Deque actualNotInOrder = new ArrayDeque<>();
for (int expectedIndex = 0; expectedIndex < expectedList.size(); expectedIndex++) {
Object expected = expectedList.get(expectedIndex);
// Find the first actual element which matches.
@NullableDecl
RepeatedField.PairResult matchingResult =
findMatchingPairResult(
actualIndices,
actualList,
expectedIndex,
expected,
excludeNonRecursive,
fieldDescriptor,
config);
if (matchingResult != null) {
// Move all prior elements to actualNotInOrder.
while (!actualIndices.isEmpty()
&& actualIndices.getFirst() < matchingResult.actualFieldIndex().get()) {
actualNotInOrder.add(actualIndices.removeFirst());
}
builder.addPairResult(matchingResult);
} else {
// Otherwise, see if a previous element matches, so we can improve the diff.
matchingResult =
findMatchingPairResult(
actualNotInOrder,
actualList,
expectedIndex,
expected,
excludeNonRecursive,
fieldDescriptor,
config);
if (matchingResult != null) {
// Report an out-of-order match, which is treated as not-matched.
matchingResult = matchingResult.toBuilder().setResult(Result.MOVED_OUT_OF_ORDER).build();
builder.addPairResult(matchingResult);
} else {
// Report a missing expected element.
builder.addPairResult(
RepeatedField.PairResult.newBuilder()
.setResult(Result.REMOVED)
.setFieldDescriptor(fieldDescriptor)
.setExpected(expected)
.setExpectedFieldIndex(expectedIndex)
.build());
}
}
}
// Report any remaining not-in-order elements as ignored.
for (int index : actualNotInOrder) {
builder.addPairResult(
RepeatedField.PairResult.newBuilder()
.setResult(Result.IGNORED)
.setFieldDescriptor(fieldDescriptor)
.setActual(actualList.get(index))
.setActualFieldIndex(index)
.build());
}
return builder.build();
}
// Given a list of values, a list of indexes into that list, and an expected value, find the first
// actual value that compares equal to the expected value, and return the PairResult for it.
// Also removes the index for the matching value from actualIndicies.
//
// If there is no match, returns null.
@NullableDecl
private RepeatedField.PairResult findMatchingPairResult(
Deque actualIndices,
List> actualValues,
int expectedIndex,
Object expectedValue,
boolean excludeNonRecursive,
FieldDescriptor fieldDescriptor,
FluentEqualityConfig config) {
Iterator actualIndexIter = actualIndices.iterator();
while (actualIndexIter.hasNext()) {
int actualIndex = actualIndexIter.next();
RepeatedField.PairResult pairResult =
compareRepeatedFieldElementPair(
actualValues.get(actualIndex),
expectedValue,
excludeNonRecursive,
fieldDescriptor,
actualIndex,
expectedIndex,
config);
if (pairResult.isMatched()) {
actualIndexIter.remove();
return pairResult;
}
}
return null;
}
private RepeatedField.PairResult compareRepeatedFieldElementPair(
@NullableDecl Object actual,
@NullableDecl Object expected,
boolean excludeNonRecursive,
FieldDescriptor fieldDescriptor,
@NullableDecl Integer actualFieldIndex,
@NullableDecl Integer expectedFieldIndex,
FluentEqualityConfig config) {
SingularField comparison =
compareSingularValue(
actual,
expected,
/*defaultValue=*/ null,
excludeNonRecursive,
fieldDescriptor,
"",
config);
RepeatedField.PairResult.Builder pairResultBuilder =
RepeatedField.PairResult.newBuilder()
.setResult(comparison.result())
.setFieldDescriptor(fieldDescriptor);
if (actual != null) {
pairResultBuilder.setActual(actual).setActualFieldIndex(actualFieldIndex);
}
if (expected != null) {
pairResultBuilder.setExpected(expected).setExpectedFieldIndex(expectedFieldIndex);
}
if (comparison.breakdown().isPresent()) {
pairResultBuilder.setBreakdown(comparison.breakdown().get());
}
return pairResultBuilder.build();
}
/** Returns a {@link LinkedHashSet} containing the integers in {@code [0, max)}, in order. */
private static Set setForRange(int max) {
Set set = Sets.newLinkedHashSet();
for (int i = 0; i < max; i++) {
set.add(i);
}
return set;
}
/**
* Compares {@code actualList} and {@code expectedList}, two submessages corresponding to {@code
* fieldDescriptor}. Uses {@code excludeNonRecursive}, {@code parentFieldPath}, and {@code
* fieldScopeLogic} to compare the messages.
*
* @return A list in index order, containing the diff results for each message.
*/
private List compareRepeatedFieldByIndices(
List> actualList,
List> expectedList,
boolean excludeNonRecursive,
FieldDescriptor fieldDescriptor,
FluentEqualityConfig config) {
int maxSize = Math.max(actualList.size(), expectedList.size());
ImmutableList.Builder builder = ImmutableList.builderWithExpectedSize(maxSize);
for (int i = 0; i < maxSize; i++) {
@NullableDecl Object actual = actualList.size() > i ? actualList.get(i) : null;
@NullableDecl Object expected = expectedList.size() > i ? expectedList.get(i) : null;
builder.add(
compareSingularValue(
actual,
expected,
/*defaultValue=*/ null,
excludeNonRecursive,
fieldDescriptor,
indexedName(fieldDescriptor, i),
config));
}
return builder.build();
}
private SingularField compareSingularValue(
@NullableDecl Object actual,
@NullableDecl Object expected,
@NullableDecl Object defaultValue,
boolean excludeNonRecursive,
FieldDescriptor fieldDescriptor,
String fieldName,
FluentEqualityConfig config) {
if (fieldDescriptor.getJavaType() == JavaType.MESSAGE) {
return compareSingularMessage(
(Message) actual,
(Message) expected,
(Message) defaultValue,
excludeNonRecursive,
fieldDescriptor,
fieldName,
config);
} else if (excludeNonRecursive) {
return SingularField.ignored(fieldName);
} else {
return compareSingularPrimitive(
actual, expected, defaultValue, fieldDescriptor, fieldName, config);
}
}
// Replaces 'input' with 'defaultValue' iff input is null and we're ignoring field absence.
// Otherwise, just returns the input.
@NullableDecl
private static T orIfIgnoringFieldAbsence(
@NullableDecl T input, @NullableDecl T defaultValue, boolean ignoreFieldAbsence) {
return (input == null && ignoreFieldAbsence) ? defaultValue : input;
}
// Returns 'input' if it's non-null, otherwise the default instance of 'other'.
// Requires at least one parameter is non-null.
private static Message orDefaultForType(
@NullableDecl Message input, @NullableDecl Message other) {
return (input != null) ? input : other.getDefaultInstanceForType();
}
private SingularField compareSingularMessage(
@NullableDecl Message actual,
@NullableDecl Message expected,
@NullableDecl Message defaultValue,
boolean excludeNonRecursive,
FieldDescriptor fieldDescriptor,
String fieldName,
FluentEqualityConfig config) {
Result.Builder result = Result.builder();
// Use the default if it's set and we're ignoring field absence.
boolean ignoreFieldAbsence =
config
.ignoreFieldAbsenceScope()
.contains(
rootDescriptor, FieldDescriptorOrUnknown.fromFieldDescriptor(fieldDescriptor));
actual = orIfIgnoringFieldAbsence(actual, defaultValue, ignoreFieldAbsence);
expected = orIfIgnoringFieldAbsence(expected, defaultValue, ignoreFieldAbsence);
// If actual or expected is missing here, we know our result so long as it's not ignored.
result.markRemovedIf(actual == null);
result.markAddedIf(expected == null);
// Perform the detailed breakdown only if necessary.
@NullableDecl DiffResult breakdown = null;
if (result.build() == Result.MATCHED || excludeNonRecursive) {
actual = orDefaultForType(actual, expected);
expected = orDefaultForType(expected, actual);
breakdown = diffMessages(actual, expected, config);
if (breakdown.isIgnored() && excludeNonRecursive) {
// Ignore this field entirely, report nothing.
return SingularField.ignored(fieldName);
}
result.markModifiedIf(!breakdown.isMatched());
}
// Report the full breakdown.
SingularField.Builder singularFieldBuilder =
SingularField.newBuilder()
.setFieldDescriptorOrUnknown(
FieldDescriptorOrUnknown.fromFieldDescriptor(fieldDescriptor))
.setFieldName(fieldName)
.setResult(result.build());
if (actual != null) {
singularFieldBuilder.setActual(actual);
}
if (expected != null) {
singularFieldBuilder.setExpected(expected);
}
if (breakdown != null) {
singularFieldBuilder.setBreakdown(breakdown);
}
return singularFieldBuilder.build();
}
private SingularField compareSingularPrimitive(
@NullableDecl Object actual,
@NullableDecl Object expected,
@NullableDecl Object defaultValue,
FieldDescriptor fieldDescriptor,
String fieldName,
FluentEqualityConfig config) {
Result.Builder result = Result.builder();
// Use the default if it's set and we're ignoring field absence.
FieldDescriptorOrUnknown fieldDescriptorOrUnknown =
FieldDescriptorOrUnknown.fromFieldDescriptor(fieldDescriptor);
boolean ignoreFieldAbsence =
config.ignoreFieldAbsenceScope().contains(rootDescriptor, fieldDescriptorOrUnknown);
actual = orIfIgnoringFieldAbsence(actual, defaultValue, ignoreFieldAbsence);
expected = orIfIgnoringFieldAbsence(expected, defaultValue, ignoreFieldAbsence);
// If actual or expected is missing here, we know our result.
result.markRemovedIf(actual == null);
result.markAddedIf(expected == null);
if (actual != null && expected != null) {
if (actual instanceof Double) {
result.markModifiedIf(
!doublesEqual(
(double) actual,
(double) expected,
config.doubleCorrespondenceMap().get(rootDescriptor, fieldDescriptorOrUnknown)));
} else if (actual instanceof Float) {
result.markModifiedIf(
!floatsEqual(
(float) actual,
(float) expected,
config.floatCorrespondenceMap().get(rootDescriptor, fieldDescriptorOrUnknown)));
} else {
result.markModifiedIf(!Objects.equal(actual, expected));
}
}
SingularField.Builder singularFieldBuilder =
SingularField.newBuilder()
.setFieldDescriptorOrUnknown(
FieldDescriptorOrUnknown.fromFieldDescriptor(fieldDescriptor))
.setFieldName(fieldName)
.setResult(result.build());
if (actual != null) {
singularFieldBuilder.setActual(actual);
}
if (expected != null) {
singularFieldBuilder.setExpected(expected);
}
return singularFieldBuilder.build();
}
private static boolean doublesEqual(double x, double y, Optional> correspondence) {
if (correspondence.isPresent()) {
try {
assertThat(x).isCloseTo(y, correspondence.get());
} catch (AssertionError e) {
return false;
}
return true;
} else {
return Double.compare(x, y) == 0;
}
}
private static boolean floatsEqual(float x, float y, Optional> correspondence) {
if (correspondence.isPresent()) {
try {
assertThat(x).isCloseTo(y, correspondence.get());
} catch (AssertionError e) {
return false;
}
return true;
} else {
return Float.compare(x, y) == 0;
}
}
private UnknownFieldSetDiff diffUnknowns(
UnknownFieldSet actual, UnknownFieldSet expected, FluentEqualityConfig config) {
UnknownFieldSetDiff.Builder builder = UnknownFieldSetDiff.newBuilder();
Map actualFields = actual.asMap();
Map expectedFields = expected.asMap();
for (int fieldNumber : Sets.union(actualFields.keySet(), expectedFields.keySet())) {
@NullableDecl UnknownFieldSet.Field actualField = actualFields.get(fieldNumber);
@NullableDecl UnknownFieldSet.Field expectedField = expectedFields.get(fieldNumber);
for (UnknownFieldDescriptor.Type type : UnknownFieldDescriptor.Type.all()) {
List> actualValues =
actualField != null ? type.getValues(actualField) : Collections.emptyList();
List> expectedValues =
expectedField != null ? type.getValues(expectedField) : Collections.emptyList();
if (actualValues.isEmpty() && expectedValues.isEmpty()) {
continue;
}
UnknownFieldDescriptor unknownFieldDescriptor =
UnknownFieldDescriptor.create(fieldNumber, type);
FieldDescriptorOrUnknown fieldDescriptorOrUnknown =
FieldDescriptorOrUnknown.fromUnknown(unknownFieldDescriptor);
FieldScopeResult compareFields =
config.compareFieldsScope().policyFor(rootDescriptor, fieldDescriptorOrUnknown);
if (compareFields == FieldScopeResult.EXCLUDED_RECURSIVELY) {
builder.addSingularField(
fieldNumber, SingularField.ignored(name(unknownFieldDescriptor)));
continue;
}
builder.addAllSingularFields(
fieldNumber,
compareUnknownFieldList(
actualValues,
expectedValues,
compareFields == FieldScopeResult.EXCLUDED_NONRECURSIVELY,
unknownFieldDescriptor,
config.subScope(rootDescriptor, fieldDescriptorOrUnknown)));
}
}
return builder.build();
}
private List compareUnknownFieldList(
List> actualValues,
List> expectedValues,
boolean excludeNonRecursive,
UnknownFieldDescriptor unknownFieldDescriptor,
FluentEqualityConfig config) {
int maxSize = Math.max(actualValues.size(), expectedValues.size());
ImmutableList.Builder builder = ImmutableList.builderWithExpectedSize(maxSize);
for (int i = 0; i < maxSize; i++) {
@NullableDecl Object actual = actualValues.size() > i ? actualValues.get(i) : null;
@NullableDecl Object expected = expectedValues.size() > i ? expectedValues.get(i) : null;
builder.add(
compareUnknownFieldValue(
actual,
expected,
excludeNonRecursive,
unknownFieldDescriptor,
indexedName(unknownFieldDescriptor, i),
config));
}
return builder.build();
}
private SingularField compareUnknownFieldValue(
@NullableDecl Object actual,
@NullableDecl Object expected,
boolean excludeNonRecursive,
UnknownFieldDescriptor unknownFieldDescriptor,
String fieldName,
FluentEqualityConfig config) {
if (unknownFieldDescriptor.type() == UnknownFieldDescriptor.Type.GROUP) {
return compareUnknownFieldSet(
(UnknownFieldSet) actual,
(UnknownFieldSet) expected,
excludeNonRecursive,
unknownFieldDescriptor,
fieldName,
config);
} else {
checkState(!excludeNonRecursive, "excludeNonRecursive is not a valid for primitives.");
return compareUnknownPrimitive(actual, expected, unknownFieldDescriptor, fieldName);
}
}
private SingularField compareUnknownFieldSet(
@NullableDecl UnknownFieldSet actual,
@NullableDecl UnknownFieldSet expected,
boolean excludeNonRecursive,
UnknownFieldDescriptor unknownFieldDescriptor,
String fieldName,
FluentEqualityConfig config) {
Result.Builder result = Result.builder();
// If actual or expected is missing, we know the result as long as it's not ignored.
result.markRemovedIf(actual == null);
result.markAddedIf(expected == null);
// Perform the detailed breakdown only if necessary.
@NullableDecl UnknownFieldSetDiff unknownsBreakdown = null;
if (result.build() == Result.MATCHED || excludeNonRecursive) {
actual = firstNonNull(actual, UnknownFieldSet.getDefaultInstance());
expected = firstNonNull(expected, UnknownFieldSet.getDefaultInstance());
unknownsBreakdown = diffUnknowns(actual, expected, config);
if (unknownsBreakdown.isIgnored() && excludeNonRecursive) {
// Ignore this field entirely, report nothing.
return SingularField.ignored(fieldName);
}
result.markModifiedIf(!unknownsBreakdown.isMatched());
}
// Report the full breakdown.
SingularField.Builder singularFieldBuilder =
SingularField.newBuilder()
.setFieldDescriptorOrUnknown(
FieldDescriptorOrUnknown.fromUnknown(unknownFieldDescriptor))
.setFieldName(fieldName)
.setResult(result.build());
if (actual != null) {
singularFieldBuilder.setActual(actual);
}
if (expected != null) {
singularFieldBuilder.setExpected(expected);
}
if (unknownsBreakdown != null) {
singularFieldBuilder.setUnknownsBreakdown(unknownsBreakdown);
}
return singularFieldBuilder.build();
}
private static SingularField compareUnknownPrimitive(
@NullableDecl Object actual,
@NullableDecl Object expected,
UnknownFieldDescriptor unknownFieldDescriptor,
String fieldName) {
Result.Builder result = Result.builder();
result.markRemovedIf(actual == null);
result.markAddedIf(expected == null);
result.markModifiedIf(!Objects.equal(actual, expected));
SingularField.Builder singularFieldBuilder =
SingularField.newBuilder()
.setFieldDescriptorOrUnknown(
FieldDescriptorOrUnknown.fromUnknown(unknownFieldDescriptor))
.setFieldName(fieldName)
.setResult(result.build());
if (actual != null) {
singularFieldBuilder.setActual(actual);
}
if (expected != null) {
singularFieldBuilder.setExpected(expected);
}
return singularFieldBuilder.build();
}
private static String name(FieldDescriptor fieldDescriptor) {
return fieldDescriptor.isExtension() ? "[" + fieldDescriptor + "]" : fieldDescriptor.getName();
}
private static String name(UnknownFieldDescriptor unknownFieldDescriptor) {
return String.valueOf(unknownFieldDescriptor.fieldNumber());
}
private static String indexedName(
FieldDescriptor fieldDescriptor, Object key, FieldDescriptor keyFieldDescriptor) {
StringBuilder sb = new StringBuilder();
try {
TextFormat.printFieldValue(keyFieldDescriptor, key, sb);
} catch (IOException impossible) {
throw new AssertionError(impossible);
}
return name(fieldDescriptor) + "[" + sb + "]";
}
private static String indexedName(FieldDescriptor fieldDescriptor, int index) {
return name(fieldDescriptor) + "[" + index + "]";
}
private static String indexedName(UnknownFieldDescriptor unknownFieldDescriptor, int index) {
return name(unknownFieldDescriptor) + "[" + index + "]";
}
}