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

com.google.common.truth.extensions.proto.MessageDifferencer Maven / Gradle / Ivy

There is a newer version: 1.4.4
Show newest version
/*
 * Copyright (c) 2016 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 com.google.common.truth.extensions.proto;

import com.google.auto.value.AutoValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableCollection;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;
import com.google.common.truth.extensions.proto.MessageDifferencer.FieldComparator.ComparisonResult;
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 com.google.protobuf.WireFormat;
import java.io.IOException;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import javax.annotation.Nullable;
import javax.annotation.concurrent.Immutable;

/**
 * Static methods and classes for comparing Protocol Messages.
 * Port of C++ version in {@code //net/proto2/util/public/message_differencer.h}
 *
 * @author [email protected] (Chris Nokleberg)
 */
@Immutable final class MessageDifferencer {

  /**
   * MapKeyComparator is used to determine if two elements have the same key
   * when comparing elements of a repeated field as a map.
   */
  public interface MapKeyComparator {
    /**
     * Decides whether the given messages match with respect to the keys of the
     * map entries they represent.
     *
     * @param parentFields the stack of SpecificFields corresponding to the proto
     * path to the given messages.
     */
    public boolean isMatch(
        MessageDifferencer messageDifferencer,
        Message message1,
        Message message2,
        List parentFields);
  }

  private static class ProtoMapKeyComparator implements MapKeyComparator {
    @Override public boolean isMatch(
        MessageDifferencer messageDifferencer, Message message1, Message message2,
        List parentFields) {
      FieldDescriptor keyField = message1.getDescriptorForType().findFieldByName("key");
      return messageDifferencer.compareFieldValueUsingParentFields(
          message1, message2,
          // -1 indices because there is no way to declare a map key as repeated.
          keyField, -1, -1,
          null, parentFields);
    }
  }
  private static final ProtoMapKeyComparator PROTO_MAP_KEY_COMPARATOR = new ProtoMapKeyComparator();

  /**
   * When comparing a repeated field as map, MultipleFieldMapKeyComparator can be used to specify
   * multiple fields as key for key comparison. Two elements of a repeated field will be regarded as
   * having the same key iff they have the same value for every specified key field.
   * Note that you can also specify only one field as key.
   */
  private static class MultipleFieldsMapKeyComparator implements MapKeyComparator {
    private final List keyFields;

    public MultipleFieldsMapKeyComparator(List key) {
      this.keyFields = key;
    }

    public MultipleFieldsMapKeyComparator(FieldDescriptor fieldDescriptor) {
      keyFields = new LinkedList<>();
      keyFields.add(fieldDescriptor);
    }

    @Override
    public boolean isMatch(
        MessageDifferencer messageDifferencer, Message message1, Message message2,
        List parentFields) {
      for (int i = 0; i < keyFields.size(); ++i) {
        FieldDescriptor field = keyFields.get(i);
        if (field.isRepeated()) {
          if (!messageDifferencer.compareRepeatedField(
                  message1, message2, field, null, parentFields)) {
            return false;
          }
        } else {
          if (!messageDifferencer.compareFieldValueUsingParentFields(
                  message1, message2, field, -1, -1, null, parentFields)) {
            return false;
          }
        }
      }
      return true;
    }
  }

  /** Creates a new builder. */
  public static Builder newBuilder() {
    return new Builder();
  }

  /** Builder object for {@link MessageDifferencer}. */
  public static final class Builder {
    private final Set setFields = Sets.newHashSet();
    private final Set ignoreFields = Sets.newHashSet();
    private final Map mapKeyComparatorMap = Maps.newHashMap();
    private MessageFieldComparison messageFieldComparison = MessageFieldComparison.EQUAL;
    private Scope scope = Scope.FULL;
    private FloatComparison floatComparison = FloatComparison.EXACT;
    private RepeatedFieldComparison repeatedFieldComparison = RepeatedFieldComparison.AS_LIST;
    private boolean reportMatches;
    private FieldComparator fieldComparator;
    private final List ignoreCriterias = Lists.newArrayList();

    private Builder() {}

    /**
     * The elements of the given repeated field will be treated as a set for
     * diffing purposes, so different orderings of the same elements will be
     * considered equal.  Elements which are present on both sides of the
     * comparison but which have changed position will be reported with {@link
     * ReportType#MOVED}.  Elements which only exist on one side or the other
     * are reported with {@link ReportType#ADDED} and {@link ReportType#DELETED}
     * regardless of their positions.  {@link ReportType#MODIFIED} is never used
     * for this repeated field.  If the only differences between the compared
     * messages is that some fields have been moved, then {@link #compare} will
     * return true.
     *
     * 

If the scope of comparison is set to {@link Scope#PARTIAL}, extra * values added to repeated fields of the second message will not cause * {@link #compare} to return false. * * @throws IllegalArgumentException if the field is not repeated or is * is already being as a map for comparison */ public Builder treatAsSet(FieldDescriptor field) { Preconditions.checkArgument(field.isRepeated(), "Field must be repeated: %s", field.getFullName()); Preconditions.checkArgument(!mapKeyComparatorMap.containsKey(field), "Cannot treat this repeated field as both Map and Set for comparison: %s", field.getFullName()); setFields.add(field); return this; } /** * The elements of the given repeated field will be treated as a map for * diffing purposes, with {@code key} being the map key. Thus, elements * with the same key will be compared even if they do not appear at the same * index. Differences are reported similarly to {@link #treatAsSet}, except * that {@link ReportType#MODIFIED} is used to report elements with the same * key but different values. Note that if an element is both moved and * modified, only {@link ReportType#MODIFIED} will be used. As with {@link * #treatAsSet}, if the only differences between the compared messages is * that some fields have been moved, then {@link #compare} will return true. * * @throws IllegalArgumentException if the field is not repeated, is not a * message, is already being as a set for comparison, or is not a * containing type of the key */ public Builder treatAsMap(FieldDescriptor field, FieldDescriptor key) { Preconditions.checkArgument(field.isRepeated(), "Field must be repeated: %s", field.getFullName()); Preconditions.checkArgument(field.getJavaType() == JavaType.MESSAGE, "Field has to be message type: %s", field.getFullName()); Preconditions.checkArgument(key.getContainingType().equals(field.getMessageType()), "%s must be a direct subfield within the repeated field: %s", key.getFullName(), field.getFullName()); Preconditions.checkArgument(!setFields.contains(field), "Cannot treat this repeated field as both Map and Set for comparison: %s", key.getFullName()); MultipleFieldsMapKeyComparator keyComparator = new MultipleFieldsMapKeyComparator(key); mapKeyComparatorMap.put(field, keyComparator); return this; } public Builder treatAsMapWithMultipleFieldsAsKey(FieldDescriptor field, List keyFields) { Preconditions.checkArgument(field.isRepeated(), "Field must be repeated " + field.getFullName()); Preconditions.checkArgument(JavaType.MESSAGE.equals(field.getJavaType()), "Field has to be message type. Field name is: " + field.getFullName()); for (int i = 0; i < keyFields.size(); ++i) { FieldDescriptor key = keyFields.get(i); Preconditions.checkArgument( key.getContainingType().equals(field.getMessageType()), key.getFullName() + " must be a direct subfield within the repeated field: " + field.getFullName()); } Preconditions.checkArgument(!setFields.contains(field), "Cannot treat this repeated field as both Map and Set for comparison."); MapKeyComparator keyComparator = new MultipleFieldsMapKeyComparator(keyFields); mapKeyComparatorMap.put(field, keyComparator); return this; } public Builder treatAsMapUsingKeyComparator( FieldDescriptor field, MapKeyComparator keyComparator) { Preconditions.checkArgument(field.isRepeated(), "Field must be repeated " + field.getFullName()); Preconditions.checkArgument(JavaType.MESSAGE.equals(field.getJavaType()), "Field has to be message type. Field name is: " + field.getFullName()); Preconditions.checkArgument(!setFields.contains(field), "Cannot treat this repeated field as both Map and Set for comparison."); mapKeyComparatorMap.put(field, keyComparator); return this; } /** * Indicates that any field with the given descriptor should be * ignored for the purposes of comparing two messages. This applies * to fields nested in the message structure as well as top level * ones. When the MessageDifferencer encounters an ignored field, * it is reported with {@link ReportType#IGNORED}. * *

The only place where the field's 'ignored' status is not applied is when * it is being used as a key in a field passed to TreatAsMap or is one of * the fields passed to TreatAsMapWithMultipleFieldsAsKey. * In this case it is compared in key matching but after that it's ignored * in value comparison. */ public Builder ignoreField(FieldDescriptor field) { ignoreFields.add(field); return this; } public Builder addIgnoreCriteria(IgnoreCriteria criterion) { this.ignoreCriterias.add(criterion); return this; } /** * Sets the type of comparison that is used by the differencer when * determining how to compare fields in messages. */ public Builder setMessageFieldComparison(MessageFieldComparison comparison) { messageFieldComparison = comparison; return this; } /** * Tells the differencer whether or not to report matches. Defaults to * false. */ public Builder setReportMatches(boolean reportMatches) { this.reportMatches = reportMatches; return this; } /** * Sets the scope of the comparison that is used by the differencer when * determining which fields to compare between the messages. Defaults to * {@link Scope#FULL}. */ public Builder setScope(Scope scope) { this.scope = scope; return this; } /** * Sets the type of comparison that is used by the differencer when * comparing float (and double) fields in messages. Defaults to {@link * FloatComparison#EXACT}. * *

If you use {@link Builder#setFieldComparator(FieldComparator)}, * this operation will be ignored */ public Builder setFloatComparison(FloatComparison comparison) { floatComparison = Preconditions.checkNotNull( comparison, "FloatComparison should not be null."); return this; } /** * Sets the {@link FieldComparator} used to determine differences between protocol * buffer fields. By default it's set to a {@link DefaultFieldComparator} instance. * Note that this method must be called before Compare for the comparator to * be used. */ public Builder setFieldComparator(FieldComparator fieldComparator) { this.fieldComparator = fieldComparator; return this; } /** * Sets the type of comparison for repeated field that is used by this * differencer when compare repeated fields in messages. Defaults to * {@link RepeatedFieldComparison#AS_LIST}. */ public Builder setRepeatedFieldComparison(RepeatedFieldComparison comparison) { repeatedFieldComparison = comparison; return this; } IgnoreCriteria getMergedIgnoreCriteria() { if (!ignoreFields.isEmpty()) { IgnoreCriteria criterion = ignoringFields(ImmutableSet.copyOf(ignoreFields)); return mergeCriteria(Iterables.concat(ignoreCriterias, Collections.singleton(criterion))); } else { return mergeCriteria(ignoreCriterias); } } /** Creates a new immutable differencer instance from this builder. */ public MessageDifferencer build() { return new MessageDifferencer(this); } } private final ImmutableSet setFields; private final IgnoreCriteria ignoreCriteria; private final ImmutableMap mapKeyComparatorMap; private final MessageFieldComparison messageFieldComparison; private final Scope scope; private final FloatComparison floatComparison; private final RepeatedFieldComparison repeatedFieldComparison; private final boolean reportMatches; private final FieldComparator fieldComparator; private MessageDifferencer(Builder builder) { setFields = ImmutableSet.copyOf(builder.setFields); ignoreCriteria = builder.getMergedIgnoreCriteria(); mapKeyComparatorMap = ImmutableMap.copyOf(builder.mapKeyComparatorMap); messageFieldComparison = builder.messageFieldComparison; scope = builder.scope; floatComparison = builder.floatComparison; repeatedFieldComparison = builder.repeatedFieldComparison; reportMatches = builder.reportMatches; fieldComparator = builder.fieldComparator == null ? new DefaultFieldComparator(floatComparison) : builder.fieldComparator; } /** * Determines whether the supplied messages are equal. Equality is defined as * all fields within the two messages being set to the same value. Primitive * fields and strings are compared by value while embedded messages/groups are * compared as if via a recursive call. * * @throws IllegalArgumentException if the messages have different descriptors */ public static boolean equals(Message message1, Message message2) { return newBuilder().build().compare(message1, message2); } /** * Determines whether the supplied messages are equivalent. Equivalency is * defined as all fields within the two messages having the same value. This * differs from the {@link #equals(Message, Message)} method above in that * fields with default values are considered set to said value * automatically. This method also ignores unknown fields. * * @throws IllegalArgumentException if the messages have different descriptors */ public static boolean equivalent(Message message1, Message message2) { return newBuilder() .setMessageFieldComparison(MessageFieldComparison.EQUIVALENT) .build() .compare(message1, message2); } /** * Determines whether the supplied messages are approximately equal. * Approximate equality is defined as all fields within the two messages being * approximately equal. Primitive (non-float) fields and strings are compared * by value, floats are compared using an equivalent of C++ {@code * MathUtil::AlmostEquals} and embedded messages/groups are compared as if via * a recursive call. * * @throws IllegalArgumentException if the messages have different descriptors */ public static boolean approximatelyEquals(Message message1, Message message2) { return newBuilder() .setFloatComparison(FloatComparison.APPROXIMATE) .build() .compare(message1, message2); } /** * Determines whether the supplied messages are approximately equivalent. * Approximate equivalency is defined as all fields within the two messages * being approximately equivalent. As in {@link #approximatelyEquals}, * primitive (non-float) fields and strings are compared by value, floats are * compared using an equivalent of C++ {@code MathUtil::AlmostEquals} and * embedded messages/groups are compared as if via a recursive call. However, * fields with default values are considered set to said value, as per {@link * #equivalent}. * * @throws IllegalArgumentException if the messages have different descriptors */ public static boolean approximatelyEquivalent(Message message1, Message message2) { return newBuilder() .setMessageFieldComparison(MessageFieldComparison.EQUIVALENT) .setFloatComparison(FloatComparison.APPROXIMATE) .build() .compare(message1, message2); } /** * IgnoreCriteria are registered with addIgnoreCriteria. For each compared field isIgnored is * called on each criterion until one returns true or all return false. isIgnored is called * for fields where at least one side has a value. */ public interface IgnoreCriteria { /** * Should this field be ignored during the comparison. * * @param message1 the message containing the field being compared * @param message2 the message containing the field being compared * @param fieldDescriptor the field being compared (null for unknown fields). * More details about unknown field is available in the last entry of fieldPath. * @param fieldPath an unmodifiable view of the path from the root message to this field * @return whether this field should be ignored in the comparison. */ boolean isIgnored(Message message1, Message message2, @Nullable FieldDescriptor fieldDescriptor, List fieldPath); } private static IgnoreCriteria ignoringFields( final ImmutableCollection fieldDescriptors) { return new IgnoreCriteria() { @Override public boolean isIgnored(Message message1, Message message2, FieldDescriptor fieldDescriptor, List fieldPath) { return fieldDescriptors.contains(fieldDescriptor); } }; } static IgnoreCriteria mergeCriteria(final Iterable criteria) { return new IgnoreCriteria() { @Override public boolean isIgnored(Message message1, Message message2, FieldDescriptor fieldDescriptor, List fieldPath) { for (IgnoreCriteria criterion : criteria) { if (criterion.isIgnored(message1, message2, fieldDescriptor, fieldPath)) { return true; } } return false; } }; } /** Identifies an individual field in a message instance. */ @AutoValue @Immutable public abstract static class SpecificField { private static SpecificField forField(FieldDescriptor field) { Preconditions.checkNotNull(field); return new AutoValue_MessageDifferencer_SpecificField(field, null, -1, -1); } private static SpecificField forRepeatedField(FieldDescriptor field, int index) { Preconditions.checkNotNull(field); Preconditions.checkArgument(index >= 0); return new AutoValue_MessageDifferencer_SpecificField(field, null, index, index); } private static SpecificField forRepeatedField(FieldDescriptor field, int index, int newIndex) { Preconditions.checkNotNull(field); Preconditions.checkArgument(index >= 0); Preconditions.checkArgument(newIndex >= 0); return new AutoValue_MessageDifferencer_SpecificField(field, null, index, newIndex); } private static SpecificField forUnknownDescriptor(UnknownDescriptor unknown, int index) { Preconditions.checkNotNull(unknown); return new AutoValue_MessageDifferencer_SpecificField(null, unknown, index, index); } /** Returns the descriptor for known fields, or null for unknown fields. */ @Nullable public abstract FieldDescriptor getField(); /** Returns the descriptor for unknown fields, or null for known fields. */ @Nullable public abstract UnknownDescriptor getUnknown(); /** * Returns the field index. If this a repeated field, this is the index * within it. For unknown fields, this is the index of the field among all * unknown fields of the same field number and type. For other fields, * returns -1. */ public abstract int getIndex(); /** * Returns the new field index. If this field is a repeated field which is * being treated as a map or a set, this indicates the position to which the * element has been moved. This only applies to {@link ReportType#MOVED}, * and (in the case of {@link Builder#treatAsMap}) {@link * ReportType#MODIFIED}. */ public abstract int getNewIndex(); } /** Unknown field information. */ @AutoValue @Immutable public abstract static class UnknownDescriptor { private static UnknownDescriptor create(int fieldNumber, UnknownFieldType fieldType) { return new AutoValue_MessageDifferencer_UnknownDescriptor(fieldNumber, fieldType); } /** Returns the field number. */ public abstract int getFieldNumber(); /** Returns the field type. */ public abstract UnknownFieldType getFieldType(); } /** * Interface for comparing protocol buffer fields. * Regular users should consider using {@link DefaultFieldComparator} * rather than this interface. * Currently, this does not support comparing unknown fields. */ public interface FieldComparator { /** * Comparison result for {@link FieldComparator#compare} */ public enum ComparisonResult { /** * Compared fields are equal. In case of comparing submessages, * user should not recursively compare their contents. */ SAME, /** * Compared fields are different. In case of comparing submessages, * user should not recursively compare their contents. */ DIFFERENT, /** * Compared submessages need to be compared recursively. * FieldComparator does not specify the semantics of recursive comparison. * This value should not be returned for simple values. */ RECURSE; /** * Return {@link ComparisonResult} from a boolean value. * * @return {@link ComparisonResult#SAME} if result is true, * {@link ComparisonResult#DIFFERENT} if result is false. */ public static ComparisonResult of(boolean result) { return result ? SAME : DIFFERENT; } } /** * Compares the values of a field in two protocol buffer messages. * * @param message1 the first message. * @param message2 the second message. * @param field field descriptor of the field where need to be compared. * @param index1 the index of first message. In case the given FieldDescriptor * points to a repeated field, the indices need to be valid. Otherwise * they should be ignored. * @param index2 the index of second message. In case the given FieldDescriptor * points to a repeated field, the indices need to be valid. Otherwise * they should be ignored. * @param parentFields an immutable list of fields that was taken to find * the current field (not include current field). * @return Returns SAME or DIFFERENT for simple values, and SAME, DIFFERENT * or RECURSE for submessages. Returning RECURSE for fields not being * submessages is illegal. */ ComparisonResult compare(Message message1, Message message2, FieldDescriptor field, int index1, int index2, ImmutableList parentFields); } /** * Interface by which callers can receive information about each difference. */ public interface Reporter { /** * Reports information about a specific field. * * @param type the type of difference * @param message1 the first message * @param message2 the second message * @param fieldPath an immutable list of fields that was taken to find * the current field. For example, for a field found in an embedded * message, the list will contain two field descriptors. The first will * be the field of the embedded message itself and the second will be * the actual field in the embedded message that was * added/deleted/modified. */ void report(ReportType type, Message message1, Message message2, ImmutableList fieldPath); } /** The type of the reported difference. */ public enum ReportType { /** A field has been added to {@code message2}. */ ADDED, /** A field has been deleted in {@code message2}. */ DELETED, IGNORED, /** A field has been modified. */ MODIFIED, /** * A repeated field has been moved to another location. This only applies * when using {@link Builder#treatAsSet} or {@link Builder#treatAsMap}. * Also note that for any given field, {@link #MODIFIED} and {@link #MOVED} * are mutually exclusive. If a field has been both moved and modified, * then only {@link #MODIFIED} will be used. */ MOVED, /** * Reports that two fields match. Useful for doing side-by-side diffs. This * is mutually exclusive with {@link #MODIFIED} and {@link #MOVED}. Matches * must be enabled using {@link Builder#setReportMatches}. */ MATCHED } /** * The type of comparison that is used by the differencer when determining * how to compare fields in messages. */ public enum MessageFieldComparison { /** * Fields must be present in both messages for the messages to be considered * the same. */ EQUAL, /** * Fields with default values are considered set for comparison purposes * even if not explicitly set in the messages themselves. Unknown fields * are ignored. */ EQUIVALENT } /** Which fields to consider when comparing messages. */ public enum Scope { /** All fields of both messages are considered in the comparison. */ FULL, /** * Only fields present in the first message are considered; fields set only * in the second message will be skipped during comparison. */ PARTIAL } /** How float and double fields in messages are compared. */ public enum FloatComparison { /** Floats and doubles are compared exactly. */ EXACT, /** * Floats and doubles are compared using an equivalent of C++ {@code * MathUtil::AlmostEqual}. */ APPROXIMATE } /** How to compare repeated fields. */ public enum RepeatedFieldComparison { /** * Repeated fields are compared in order. Differing values at the same * index are reported using ReportModified(). If the repeated fields have * different numbers of elements, the unpaired elements are reported using * {@link ReportType#ADDED} or {@link ReportType#DELETED}. */ AS_LIST, /** * Treat all the repeated fields as sets by default. See {@link * Builder#treatAsSet}. */ AS_SET } /** The wire type of unknown fields. */ public enum UnknownFieldType { /** Varint. */ VARINT(WireFormat.WIRETYPE_VARINT) { @Override public List getValues(UnknownFieldSet.Field field) { return field.getVarintList(); } }, /** Fixed32. */ FIXED32(WireFormat.WIRETYPE_FIXED32) { @Override public List getValues(UnknownFieldSet.Field field) { return field.getFixed32List(); } }, /** Fixed64. */ FIXED64(WireFormat.WIRETYPE_FIXED64) { @Override public List getValues(UnknownFieldSet.Field field) { return field.getFixed64List(); } }, /** Length delimited. */ LENGTH_DELIMITED(WireFormat.WIRETYPE_LENGTH_DELIMITED) { @Override public List getValues(UnknownFieldSet.Field field) { return field.getLengthDelimitedList(); } }, /** Group. */ GROUP(WireFormat.WIRETYPE_START_GROUP) { @Override public List getValues(UnknownFieldSet.Field field) { return field.getGroupList(); } }; final int wireFormat; UnknownFieldType(int wireFormat) { this.wireFormat = wireFormat; } /** Returns the wire format for this unknown field type. */ public int getWireFormat() { return wireFormat; } // TODO(chrisn): Genericize UnknownFieldType based on value type? /** Returns the corresponding values from the given field. */ public abstract List getValues(UnknownFieldSet.Field field); } /** * Compares the two specified messages, returning true if they are the same. * * @throws IllegalArgumentException if the messages have different descriptors */ public boolean compare(Message message1, Message message2) { return compare(message1, message2, null); } /** * Compares the two specified messages, returning true if they are the same. * Reports differences to the reporter if it is non-null. * * @throws IllegalArgumentException if the messages have different descriptors */ public boolean compare(Message message1, Message message2, @Nullable Reporter reporter) { List stack = Lists.newArrayList(); return compare(message1, message2, reporter, stack); } /** * Same as above, except comparing only the given sets of field descriptors, * using only the given message fields. * * @throws IllegalArgumentException if the messages have different descriptors */ public boolean compareWithFields(Message message1, Message message2, Set message1Fields, Set message2Fields) { return compareWithFields(message1, message2, message1Fields, message2Fields, null); } /** * Compares the two specified messages, returning true if they are the same, * using only the given message fields. Reports differences to the reporter if * it is non-null. * * @throws IllegalArgumentException if the messages have different descriptors */ public boolean compareWithFields(Message message1, Message message2, Set message1Fields, Set message2Fields, @Nullable Reporter reporter) { checkSameDescriptor(message1, message2); // Ensure fields are sorted. message1Fields = ImmutableSet.copyOf(Ordering.natural().sortedCopy(message1Fields)); message2Fields = ImmutableSet.copyOf(Ordering.natural().sortedCopy(message2Fields)); List stack = Lists.newArrayList(); return compareRequestedFields(message1, message2, message1Fields, message2Fields, reporter, stack); } private boolean compare(Message message1, Message message2, @Nullable Reporter reporter, List stack) { checkSameDescriptor(message1, message2); if (message1 == message2 && (reporter == null || !reportMatches)) { return true; } boolean unknownCompareResult = true; if (!compareUnknownFields(message1, message2, reporter, stack)) { if (reporter == null) { return false; } unknownCompareResult = false; } Set message1Fields = message1.getAllFields().keySet(); Set message2Fields = message2.getAllFields().keySet(); return compareRequestedFields(message1, message2, message1Fields, message2Fields, reporter, stack) && unknownCompareResult; } private void checkSameDescriptor(Message message1, Message message2) { Preconditions.checkArgument(message1.getDescriptorForType() .equals(message2.getDescriptorForType()), "Comparison between two messages with different descriptors: %s and %s", message1.getClass(), message2.getClass()); } private boolean compareUnknownFields(Message message1, Message message2, @Nullable Reporter reporter, List stack) { UnknownFieldSet unknownFieldSet1 = message1.getUnknownFields(); UnknownFieldSet unknownFieldSet2 = message2.getUnknownFields(); return compareUnknownFields(message1, message2, unknownFieldSet1, unknownFieldSet2, reporter, stack); } private boolean compareUnknownFields(Message message1, Message message2, UnknownFieldSet unknownFieldSet1, UnknownFieldSet unknownFieldSet2, @Nullable Reporter reporter, List stack) { if (messageFieldComparison == MessageFieldComparison.EQUIVALENT) { return true; } boolean identical = unknownFieldSet1.equals(unknownFieldSet2); if (identical && (reporter == null || !reportMatches)) { return true; } Set numbers1 = unknownFieldSet1.asMap().keySet(); Set numbers2 = unknownFieldSet2.asMap().keySet(); if (numbers1.isEmpty() && numbers2.isEmpty()) { return true; } boolean match = true; // Use TreeSet to visit the fields in tag order. for (Integer number : Sets.newTreeSet(Sets.union(numbers1, numbers2))) { for (UnknownFieldType fieldType : UnknownFieldType.values()) { List values1 = fieldType.getValues(unknownFieldSet1.getField(number)); List values2 = fieldType.getValues(unknownFieldSet2.getField(number)); if (values1.equals(values2)) { continue; } if (values1.isEmpty()) { if (scope == Scope.PARTIAL) { continue; } } UnknownDescriptor unknownDesc = UnknownDescriptor.create(number, fieldType); for (int i = 0, count = Math.max(values1.size(), values2.size()); i < count; i++) { Object value1 = (i < values1.size()) ? values1.get(i) : null; Object value2 = (i < values2.size()) ? values2.get(i) : null; ReportType reportType = ReportType.MATCHED; SpecificField unknownField = SpecificField.forUnknownDescriptor(unknownDesc, i); if (ignoreCriteria.isIgnored(message1, message2, null, immutable(stack, unknownField))) { if (reporter == null || !reportMatches) { continue; } reportType = ReportType.IGNORED; } else if (value1 == null) { reportType = ReportType.ADDED; match = false; } else if (value2 == null) { reportType = ReportType.DELETED; match = false; } else if (fieldType == UnknownFieldType.GROUP) { stack.add(unknownField); if (!compareUnknownFields( message1, message2, (UnknownFieldSet) value1, (UnknownFieldSet) value2, reporter, stack)) { reportType = ReportType.MODIFIED; match = false; } pop(stack); } else if (!Objects.equals(value1, value2)) { reportType = ReportType.MODIFIED; match = false; } if (reporter != null) { if (reportType != ReportType.MATCHED || reportMatches) { reporter.report(reportType, message1, message2, immutable(stack, unknownField)); } } else if (!match) { return false; } } } } return match; } private boolean compareRequestedFields(Message message1, Message message2, Set message1Fields, Set message2Fields, @Nullable Reporter reporter, List stack) { if (scope == Scope.FULL) { if (messageFieldComparison == MessageFieldComparison.EQUIVALENT) { // We need to merge the field lists of both messages (i.e. // we are merely checking for a difference in field values, // rather than the addition or deletion of fields). Set fieldsUnion = Sets.union(message1Fields, message2Fields); return compareWithFieldsInternal(message1, message2, fieldsUnion, fieldsUnion, reporter, stack); } else { // Simple equality comparison, use the unaltered field lists. return compareWithFieldsInternal(message1, message2, message1Fields, message2Fields, reporter, stack); } } else { if (messageFieldComparison == MessageFieldComparison.EQUIVALENT) { // We use the list of fields for message1 for both messages when // comparing. This way, extra fields in message2 are ignored, // and missing fields in message2 use their default value. return compareWithFieldsInternal(message1, message2, message1Fields, message1Fields, reporter, stack); } else { // We need to consider the full list of fields for message1 // but only the intersection for message2. This way, any fields // only present in message2 will be ignored, but any fields only // present in message1 will be marked as a difference. Set fieldsIntersection = Sets.intersection(message1Fields, message2Fields); return compareWithFieldsInternal(message1, message2, message1Fields, fieldsIntersection, reporter, stack); } } } private static final Set SENTINEL = Collections.singleton(null); private boolean compareWithFieldsInternal(Message message1, Message message2, Set message1Fields, Set message2Fields, @Nullable Reporter reporter, List stack) { boolean isDifferent = false; Iterator it1 = Iterables.concat(message1Fields, SENTINEL).iterator(); Iterator it2 = Iterables.concat(message2Fields, SENTINEL).iterator(); // Loop while there are any fields in either message. FieldDescriptor field1 = it1.next(); FieldDescriptor field2 = it2.next(); while (field1 != null || field2 != null) { // Check for differences in the field itself. if (fieldBefore(field1, field2)) { // Field 1 is not in the field list for message 2. if (ignoreCriteria.isIgnored( message1, message2, field1, Collections.unmodifiableList(stack))) { // We are ignoring field1. Report the ignore and move on to the next field in message1. if (reporter != null) { report(ReportType.IGNORED, message1, message2, field1, message1, reporter, stack); } field1 = it1.next(); continue; } if (reporter == null) { return false; } else { report(ReportType.DELETED, message1, message2, field1, message1, reporter, stack); isDifferent = true; } field1 = it1.next(); continue; } else if (fieldBefore(field2, field1)) { // Field 2 is not in the field list for message 1. if (ignoreCriteria.isIgnored( message1, message2, field2, Collections.unmodifiableList(stack))) { // We are ignoring field2. Report the ignore and move on to the next field in message2. if (reporter != null) { report(ReportType.IGNORED, message1, message2, field2, message2, reporter, stack); } field2 = it2.next(); continue; } if (reporter == null) { return false; } else { report(ReportType.ADDED, message1, message2, field2, message2, reporter, stack); isDifferent = true; } field2 = it2.next(); continue; } // By this point, field1 and field2 are guaranteed to point to the same // field, so we can now compare the values. boolean fieldDifferent = false; if (ignoreCriteria.isIgnored( message1, message2, field1, Collections.unmodifiableList(stack))) { if (reporter != null) { report(ReportType.IGNORED, message1, message2, field2, message2, reporter, stack); } } else if (field1.isRepeated()) { fieldDifferent = !compareRepeatedField(message1, message2, field1, reporter, stack); if (fieldDifferent) { if (reporter == null) { return false; } isDifferent = true; } } else { SpecificField specificField = SpecificField.forField(field1); fieldDifferent = !compareFieldValueUsingParentFields( message1, message2, field1, -1, -1, reporter, stack); // If we have found differences, either report them or terminate if // no reporter is present. if (fieldDifferent) { if (reporter == null) { return false; } reporter.report(ReportType.MODIFIED, message1, message2, immutable(stack, specificField)); // If the field was at any point found to be different, mark to // return this difference once the loop has completed. isDifferent = true; } else if (reportMatches && reporter != null) { reporter.report(ReportType.MATCHED, message1, message2, immutable(stack, specificField)); } } field1 = it1.next(); field2 = it2.next(); } return !isDifferent; } boolean compareFieldValueUsingParentFields(Message message1, Message message2, FieldDescriptor field, int index1, int index2, @Nullable Reporter reporter, List stack) { ComparisonResult result = fieldComparator.compare(message1, message2, field, index1, index2, ImmutableList.copyOf(stack)); if (result == ComparisonResult.RECURSE) { Preconditions.checkArgument(field.getJavaType() == JavaType.MESSAGE, "FieldComparator should not return RECURSE for fields not being submessages!"); // Get the nested messages and compare them using one of the // methods. Message nextMessage1 = field.isRepeated() ? (Message) message1.getRepeatedField(field, index1) : (Message) message1.getField(field); Message nextMessage2 = field.isRepeated() ? (Message) message2.getRepeatedField(field, index2) : (Message) message2.getField(field); stack.add(field.isRepeated() ? SpecificField.forRepeatedField(field, index1, index2) : SpecificField.forField(field)); boolean isSame = compare(nextMessage1, nextMessage2, reporter, stack); pop(stack); return isSame; } return result == ComparisonResult.SAME; } private void report(ReportType reportType, Message message1, Message message2, FieldDescriptor field, Message first, Reporter reporter, List stack) { if (field.isRepeated()) { int count = first.getRepeatedFieldCount(field); for (int i = 0; i < count; i++) { reporter.report(reportType, message1, message2, immutable(stack, SpecificField.forRepeatedField(field, i))); } } else { reporter.report(reportType, message1, message2, immutable(stack, SpecificField.forField(field))); } } private boolean fieldBefore(FieldDescriptor field1, FieldDescriptor field2) { if (field1 == null) { return false; } if (field2 == null) { return true; } return field1.getNumber() < field2.getNumber(); } boolean compareRepeatedField(Message message1, Message message2, FieldDescriptor repeatedField, @Nullable Reporter reporter, List stack) { int count1 = message1.getRepeatedFieldCount(repeatedField); int count2 = message2.getRepeatedFieldCount(repeatedField); boolean treatedAsSubset = isTreatedAsSubset(repeatedField); // If the field is not treated as subset and no detailed reports is needed, // we do a quick check on the number of the elements to avoid unnecessary // comparison. if (count1 != count2 && reporter == null && !treatedAsSubset) { return false; } // These two arrays are used for store the index of the correspondent // element in peer repeated field. int[] matchList1 = new int[count1]; int[] matchList2 = new int[count2]; // Try to match indices of the repeated fields. Return false if match fails // and there's no detailed report needed. if (!matchRepeatedFieldIndices(message1, message2, repeatedField, matchList1, matchList2, stack) && reporter == null) { return false; } boolean fieldDifferent = false; // At this point, we have already matched pairs of fields (with the reporting // to be done later). Now to check if the paired elements are different. for (int i = 0; i < count1; i++) { if (matchList1[i] == -1) { continue; } int newIndex = matchList1[i]; SpecificField specificField = SpecificField.forRepeatedField(repeatedField, i, newIndex); boolean result = compareFieldValueUsingParentFields( message1, message2, repeatedField, i, newIndex, reporter, stack); // If we have found differences, either report them or terminate if // no reporter is present. Note that ReportModified, ReportMoved, and // ReportMatched are all mutually exclusive. if (!result) { if (reporter == null) { return false; } fieldDifferent = true; } if (reporter == null) { continue; } ReportType reportType = null; if (!result) { reportType = ReportType.MODIFIED; } else if (i != newIndex) { reportType = ReportType.MOVED; } else if (reportMatches) { reportType = ReportType.MATCHED; } if (reportType != null) { reporter.report(reportType, message1, message2, immutable(stack, specificField)); } } // Report any remaining additions or deletions. for (int i = 0; i < count2; i++) { if (matchList2[i] != -1) { continue; } if (!treatedAsSubset) { fieldDifferent = true; } if (reporter != null) { reporter.report(ReportType.ADDED, message1, message2, immutable(stack, SpecificField.forRepeatedField(repeatedField, i))); } } for (int i = 0; i < count1; i++) { if (matchList1[i] != -1) { continue; } // We would have exited earlier if reporter was null. reporter.report(ReportType.DELETED, message1, message2, immutable(stack, SpecificField.forRepeatedField(repeatedField, i))); fieldDifferent = true; } return !fieldDifferent; } private boolean matchRepeatedFieldIndices(Message message1, Message message2, FieldDescriptor repeatedField, int[] matchList1, int[] matchList2, List stack) { MapKeyComparator keyComparator = mapKeyComparatorMap.get(repeatedField); if (repeatedField.isMapField() && keyComparator == null) { keyComparator = PROTO_MAP_KEY_COMPARATOR; } int count1 = matchList1.length; int count2 = matchList2.length; Arrays.fill(matchList1, -1); Arrays.fill(matchList2, -1); boolean success = true; // Find potential match if this is a special repeated field. if (keyComparator != null || isTreatedAsSet(repeatedField)) { for (int i = 0; i < count1; i++) { // Indicates any matched elements for this repeated field. boolean match = false; int newIndex = i; for (int j = 0; j < count2; j++) { if (matchList2[j] != -1) { continue; } newIndex = j; match = isMatch(repeatedField, keyComparator, message1, message2, i, j, stack); if (match) { matchList1[i] = newIndex; matchList2[newIndex] = i; break; } } success = success && match; } } else { // If this field should be treated as list, just label the match_list. for (int i = 0; i < count1 && i < count2; i++) { matchList1[i] = matchList2[i] = i; } } return success; } private boolean isMatch(FieldDescriptor repeatedField, @Nullable MapKeyComparator keyComparator, Message message1, Message message2, int index1, int index2, List stack) { boolean isSame; if (keyComparator == null) { return compareFieldValueUsingParentFields( message1, message2, repeatedField, index1, index2, null, stack); } else { Message m1 = (Message) message1.getRepeatedField(repeatedField, index1); Message m2 = (Message) message2.getRepeatedField(repeatedField, index2); stack.add(SpecificField.forRepeatedField(repeatedField, index1, index2)); isSame = keyComparator.isMatch(this, m1, m2, stack); } pop(stack); return isSame; } private boolean isTreatedAsSubset(FieldDescriptor field) { return isTreatedAsSet(field) && scope == Scope.PARTIAL; } private boolean isTreatedAsSet(FieldDescriptor field) { if (repeatedFieldComparison == RepeatedFieldComparison.AS_SET) { return true; } return setFields.contains(field); } // Returns an immutable list copy of the stack with an extra element appended. private static ImmutableList immutable(Iterable stack, T extraElement) { return ImmutableList.builder().addAll(stack).add(extraElement).build(); } // Pops the last result off of a list. private static void pop(List stack) { stack.remove(stack.size() - 1); } /** * A message difference reporter that writes a textual description of the * differences to a character stream. */ public static final class StreamReporter implements Reporter { private final Appendable output; private final boolean reportModifiedAggregates; /** Equivalent to {@code new StreamReporter(output, false)}. */ public StreamReporter(Appendable output) { this(output, false); } /** * Creates a new reporter. * * @param output where to write the output to * @param reportModifiedAggregates when set to true, the stream reporter will * also output aggregates nodes (i.e. messages and groups) whose subfields * have been modified. When false, will only report the individual * subfields. Defaults to false. */ public StreamReporter(Appendable output, boolean reportModifiedAggregates) { this.output = Preconditions.checkNotNull(output); this.reportModifiedAggregates = reportModifiedAggregates; } /** I/O exceptions that occur during reporting are wrapped by this type. */ public static final class StreamException extends RuntimeException { private StreamException(IOException e) { super(e); } } @Override public void report(ReportType type, Message message1, Message message2, ImmutableList fieldPath) { try { if (type == ReportType.MODIFIED && !reportModifiedAggregates) { SpecificField specificField = Iterables.getLast(fieldPath); if (specificField.getField() == null) { if (specificField.getUnknown().getFieldType() == UnknownFieldType.GROUP) { // Any changes to the subfields have already been printed. return; } } else if (specificField.getField().getJavaType() == JavaType.MESSAGE) { // Any changes to the subfields have already been printed. return; } } output.append(type.name().toLowerCase()).append(": "); switch (type) { case ADDED: appendPath(fieldPath, false); output.append(": "); appendValue(message2, fieldPath, false); break; case DELETED: appendPath(fieldPath, true); output.append(": "); appendValue(message1, fieldPath, true); break; case IGNORED: appendPath(fieldPath, false); break; case MOVED: appendPath(fieldPath, true); output.append(" -> "); appendPath(fieldPath, false); output.append(" : "); appendValue(message1, fieldPath, true); break; case MODIFIED: appendPath(fieldPath, true); if (checkPathChanged(fieldPath)) { output.append(" -> "); appendPath(fieldPath, false); } output.append(": "); appendValue(message1, fieldPath, true); output.append(" -> "); appendValue(message2, fieldPath, false); break; case MATCHED: appendPath(fieldPath, true); if (checkPathChanged(fieldPath)) { output.append(" -> "); appendPath(fieldPath, false); } output.append(" : "); appendValue(message1, fieldPath, true); break; } output.append("\n"); } catch (IOException e) { throw new StreamException(e); } } private boolean checkPathChanged(ImmutableList fieldPath) { for (SpecificField specificField : fieldPath) { if (specificField.getIndex() != specificField.getNewIndex()) { return true; } } return false; } private void appendPath(ImmutableList fieldPath, boolean leftSide) throws IOException { for (Iterator it = fieldPath.iterator(); it.hasNext();) { SpecificField specificField = it.next(); FieldDescriptor field = specificField.getField(); if (field != null) { if (field.isExtension()) { output.append("(").append(field.getFullName()).append(")"); } else { output.append(field.getName()); } } else { output.append(String.valueOf(specificField.getUnknown().getFieldNumber())); } if (leftSide && specificField.getIndex() >= 0) { output.append("[").append(String.valueOf(specificField.getIndex())).append("]"); } if (!leftSide && specificField.getNewIndex() >= 0) { output.append("[").append(String.valueOf(specificField.getNewIndex())).append("]"); } if (it.hasNext()) { output.append("."); } } } private void appendValue(Message message, ImmutableList fieldPath, boolean leftSide) throws IOException { SpecificField specificField = Iterables.getLast(fieldPath); FieldDescriptor field = specificField.getField(); if (field != null) { int index = leftSide ? specificField.getIndex() : specificField.getNewIndex(); Object value = field.isRepeated() ? message.getRepeatedField(field, index) : message.getField(field); if (field.getJavaType() == JavaType.MESSAGE) { output.append(wrapDebugString(TextFormat.shortDebugString((Message) value))); } else { TextFormat.printFieldValue(field, value, output); } } else { UnknownFieldSet unknownFields = message.getUnknownFields(); UnknownFieldSet.Field unknownField = null; UnknownDescriptor unknownDescriptor = null; for (SpecificField node : fieldPath) { unknownDescriptor = node.getUnknown(); if (unknownDescriptor != null) { unknownField = unknownFields.getField(unknownDescriptor.getFieldNumber()); if (unknownDescriptor.getFieldType() == UnknownFieldType.GROUP) { unknownFields = unknownField.getGroupList().get(node.getIndex()); } } } UnknownFieldType unknownType = unknownDescriptor.getFieldType(); Object value = unknownType.getValues(unknownField).get(specificField.getIndex()); int wireFormat = unknownType.getWireFormat(); if (wireFormat == WireFormat.WIRETYPE_START_GROUP) { output.append(wrapDebugString(TextFormat.shortDebugString((UnknownFieldSet) value))); } else { TextFormat.printUnknownFieldValue(wireFormat, value, output); } } } } // Wraps a message debug string in curly braces. private static String wrapDebugString(String debugString) { return debugString.isEmpty() ? "{ }" : "{ " + debugString + " }"; } /** * Basic implementation of FieldComparator. */ @Immutable public static final class DefaultFieldComparator implements FieldComparator { private final FloatComparison floatComparison; public DefaultFieldComparator(FloatComparison floatComparison) { this.floatComparison = Preconditions.checkNotNull(floatComparison); } /** Port of C++ MathUtil::AlmostEquals, with STD_ERR of 1e-5f * 32. */ @VisibleForTesting static boolean almostEquals(float x, float y) { return almostEquals(x, y, 1e-5f * 32); } /** Port of C++ MathUtil::AlmostEquals, with STD_ERR of 1e-9d * 32. */ @VisibleForTesting static boolean almostEquals(double x, double y) { return almostEquals(x, y, 1e-9d * 32); } private static boolean almostEquals(double x, double y, double stdErr) { if (x == y) { return true; } // It's convenient in many ways to treat NaN as equal to NaN - it's also // what the exact comparison does, by virtue of using Double.equals instead // of ==. if (Double.isNaN(x) && Double.isNaN(y)) { return true; } if (Double.isInfinite(x) || Double.isInfinite(y)) { return false; } if (Math.abs(x) <= stdErr && Math.abs(y) <= stdErr) { return true; } double absDiff = (x > y) ? x - y : y - x; return absDiff <= Math.max(stdErr, stdErr * Math.max(Math.abs(x), Math.abs(y))); } @Override public ComparisonResult compare(Message message1, Message message2, FieldDescriptor field, int index1, int index2, ImmutableList parentFields) { Object value1 = field.isRepeated() ? message1.getRepeatedField(field, index1) : message1.getField(field); Object value2 = field.isRepeated() ? message2.getRepeatedField(field, index2) : message2.getField(field); switch (field.getJavaType()) { case MESSAGE: return ComparisonResult.RECURSE; case INT: case LONG: case BOOLEAN: case STRING: case BYTE_STRING: case ENUM: return ComparisonResult.of(value1.equals(value2)); case FLOAT: if (floatComparison == FloatComparison.EXACT) { return ComparisonResult.of(value1.equals(value2)); } else { return ComparisonResult.of(almostEquals( ((Number) value1).floatValue(), ((Number) value2).floatValue())); } case DOUBLE: if (floatComparison == FloatComparison.EXACT) { return ComparisonResult.of(value1.equals(value2)); } else { return ComparisonResult.of(almostEquals( ((Number) value1).doubleValue(), ((Number) value2).doubleValue())); } default: throw new IllegalArgumentException("Bad field type " + field.getJavaType()); } } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy