All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.curioswitch.common.testing.assertj.proto.ProtoTruthMessageDifferencer Maven / Gradle / Ivy

There is a newer version: 0.2.1
Show newest version
/*
 * 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 + "]"; } }