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

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

The newest version!
/*
 * 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 com.google.common.truth.extensions.proto;

import com.google.auto.value.AutoValue;
import com.google.auto.value.extension.memoized.Memoized;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.ForOverride;
import com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.Message;
import com.google.protobuf.TextFormat;
import com.google.protobuf.UnknownFieldSet;
import java.io.IOException;
import java.util.Set;

/**
 * Structural summary of the difference between two messages.
 *
 * 

A {@code DiffResult} has singular fields, repeated fields, and unknowns, each with their own * class. The inner classes may also contain their own {@code DiffResult}s for submessages. * *

These classes form a recursive, hierarchical relationship. Much of the common recursion logic * across all the classes is in {@link RecursableDiffEntity}. */ @AutoValue abstract class DiffResult extends RecursableDiffEntity.WithoutResultCode { /** * Structural summary of the difference between two singular (non-repeated) fields. * *

It is possible for the result to be {@code ADDED} or {@code REMOVED}, even if {@code * actual()} and {@code expected()} are identical. This occurs if the config does not have {@link * FluentEqualityConfig#ignoringFieldAbsence()} enabled, one message had the field explicitly set * to the default value, and the other did not. */ @AutoValue abstract static class SingularField extends RecursableDiffEntity.WithResultCode implements ProtoPrintable { /** The type information for this field. May be absent if result code is {@code IGNORED}. */ abstract Optional subScopeId(); /** The display name for this field. May include an array-index specifier. */ abstract String fieldName(); /** The field under test. */ abstract Optional actual(); /** The expected value of said field. */ abstract Optional expected(); /** * The detailed breakdown of the comparison, only present if both objects are set on this * instance and they are messages. * *

This does not necessarily mean the messages were set on the input protos. */ abstract Optional breakdown(); /** * The detailed breakdown of the comparison, only present if both objects are set and they are * {@link UnknownFieldSet}s. * *

This will only ever be set inside a parent {@link UnknownFieldSetDiff}. The top {@link * UnknownFieldSetDiff} is set on the {@link DiffResult}, not here. */ abstract Optional unknownsBreakdown(); /** Returns {@code actual().get()}, or {@code expected().get()}, whichever is available. */ @Memoized Object actualOrExpected() { return actual().or(expected()).get(); } @Memoized @Override Iterable childEntities() { return ImmutableList.copyOf( Iterables.concat(breakdown().asSet(), unknownsBreakdown().asSet())); } @Override final void printContents(boolean includeMatches, String fieldPrefix, StringBuilder sb) { if (!includeMatches && isMatched()) { return; } fieldPrefix = newFieldPrefix(fieldPrefix, fieldName()); switch (result()) { case ADDED: sb.append("added: ").append(fieldPrefix).append(": "); if (actual().get() instanceof Message) { sb.append("\n"); printMessage((Message) actual().get(), sb); } else { printFieldValue(subScopeId().get(), actual().get(), sb); sb.append("\n"); } return; case IGNORED: sb.append("ignored: ").append(fieldPrefix).append("\n"); return; case MATCHED: sb.append("matched: ").append(fieldPrefix); if (actualOrExpected() instanceof Message) { sb.append("\n"); printChildContents(includeMatches, fieldPrefix, sb); } else { sb.append(": "); printFieldValue(subScopeId().get(), actualOrExpected(), sb); sb.append("\n"); } return; case MODIFIED: sb.append("modified: ").append(fieldPrefix); if (actualOrExpected() instanceof Message) { sb.append("\n"); printChildContents(includeMatches, fieldPrefix, sb); } else { sb.append(": "); printFieldValue(subScopeId().get(), expected().get(), sb); sb.append(" -> "); printFieldValue(subScopeId().get(), actual().get(), sb); sb.append("\n"); } return; case REMOVED: sb.append("deleted: ").append(fieldPrefix).append(": "); if (expected().get() instanceof Message) { sb.append("\n"); printMessage((Message) expected().get(), sb); } else { printFieldValue(subScopeId().get(), expected().get(), sb); sb.append("\n"); } return; default: throw new AssertionError("Impossible: " + result()); } } @Override final boolean isContentEmpty() { return false; } static SingularField ignored(String fieldName) { return newBuilder() .setFieldName(fieldName) .setResult(Result.IGNORED) // Ignored fields don't need a customized proto printer. .setProtoPrinter(TextFormat.printer()) .build(); } static Builder newBuilder() { return new AutoValue_DiffResult_SingularField.Builder(); } /** Builder for {@link SingularField}. */ @AutoValue.Builder abstract static class Builder { abstract Builder setResult(Result result); abstract Builder setSubScopeId(SubScopeId subScopeId); abstract Builder setFieldName(String fieldName); abstract Builder setActual(Object actual); abstract Builder setExpected(Object expected); abstract Builder setBreakdown(DiffResult breakdown); abstract Builder setUnknownsBreakdown(UnknownFieldSetDiff unknownsBreakdown); abstract Builder setProtoPrinter(TextFormat.Printer value); abstract SingularField build(); } } /** * Structural summary of the difference between two repeated fields. * *

This is only present if the user specified {@code ignoringRepeatedFieldOrder()}. Otherwise, * the repeated elements are compared as singular fields, and there are no 'move' semantics. */ @AutoValue abstract static class RepeatedField extends RecursableDiffEntity.WithoutResultCode { /** * Structural summary of the difference between two elements in two corresponding repeated * fields, in the context of a {@link RepeatedField} diff. * *

The field indexes will only be present if the corresponding object is also present. If an * object is absent, the PairResult represents an extra/missing element in the repeated field. * If both are present but the indexes differ, it represents a 'move'. */ @AutoValue abstract static class PairResult extends RecursableDiffEntity.WithResultCode implements ProtoPrintable { /** The {@link FieldDescriptor} describing the repeated field for this pair. */ abstract FieldDescriptor fieldDescriptor(); /** The index of the element in the {@code actual()} list that was matched. */ abstract Optional actualFieldIndex(); /** The index of the element in the {@code expected()} list that was matched. */ abstract Optional expectedFieldIndex(); /** The element in the {@code actual()} list that was matched. */ abstract Optional actual(); /** The element in the {@code expected()} list that was matched. */ abstract Optional expected(); /** * A detailed breakdown of the comparison between the messages. Present iff {@code actual()} * and {@code expected()} are {@link Message}s. */ abstract Optional breakdown(); @Memoized @Override Iterable childEntities() { return breakdown().asSet(); } /** Returns true if actual() and expected() contain Message types. */ @Memoized boolean isMessage() { return actual().orNull() instanceof Message || expected().orNull() instanceof Message; } private static String indexed(String fieldPrefix, Optional fieldIndex) { String index = fieldIndex.isPresent() ? fieldIndex.get().toString() : "?"; return fieldPrefix + "[" + index + "]"; } @Override final void printContents(boolean includeMatches, String fieldPrefix, StringBuilder sb) { printContentsForRepeatedField( /* includeSelfAlways = */ false, includeMatches, fieldPrefix, sb); } // When printing results for a repeated field, we want to print matches even if // !includeMatches if there's a mismatch on the repeated field itself, but not recursively. // So we define a second printing method for use by the parent. final void printContentsForRepeatedField( boolean includeSelfAlways, boolean includeMatches, String fieldPrefix, StringBuilder sb) { if (!includeSelfAlways && !includeMatches && isMatched()) { return; } switch (result()) { case ADDED: sb.append("added: ").append(indexed(fieldPrefix, actualFieldIndex())).append(": "); if (isMessage()) { sb.append("\n"); printMessage((Message) actual().get(), sb); } else { printFieldValue(fieldDescriptor(), actual().get(), sb); sb.append("\n"); } return; case IGNORED: sb.append("ignored: "); if (actualFieldIndex().equals(expectedFieldIndex())) { sb.append(indexed(fieldPrefix, actualFieldIndex())); } else { sb.append(indexed(fieldPrefix, expectedFieldIndex())) .append(" -> ") .append(indexed(fieldPrefix, actualFieldIndex())); } // We output the message contents for ignored pair results, since it's likely not clear // from the index alone why they were ignored. sb.append(":"); if (isMessage()) { sb.append("\n"); printChildContents(includeMatches, indexed(fieldPrefix, actualFieldIndex()), sb); } else { sb.append(" "); printFieldValue(fieldDescriptor(), actual().get(), sb); sb.append("\n"); } return; case MATCHED: if (actualFieldIndex().get().equals(expectedFieldIndex().get())) { sb.append("matched: ").append(indexed(fieldPrefix, actualFieldIndex())); } else { sb.append("moved: ") .append(indexed(fieldPrefix, expectedFieldIndex())) .append(" -> ") .append(indexed(fieldPrefix, actualFieldIndex())); } sb.append(":"); if (isMessage()) { sb.append("\n"); printChildContents(includeMatches, indexed(fieldPrefix, actualFieldIndex()), sb); } else { sb.append(" "); printFieldValue(fieldDescriptor(), actual().get(), sb); sb.append("\n"); } return; case MOVED_OUT_OF_ORDER: sb.append("out_of_order: ") .append(indexed(fieldPrefix, expectedFieldIndex())) .append(" -> ") .append(indexed(fieldPrefix, actualFieldIndex())); sb.append(":"); if (isMessage()) { sb.append("\n"); printChildContents(includeMatches, indexed(fieldPrefix, actualFieldIndex()), sb); } else { sb.append(" "); printFieldValue(fieldDescriptor(), actual().get(), sb); sb.append("\n"); } return; case MODIFIED: sb.append("modified: "); if (actualFieldIndex().get().equals(expectedFieldIndex().get())) { sb.append(indexed(fieldPrefix, actualFieldIndex())); } else { sb.append(indexed(fieldPrefix, expectedFieldIndex())) .append(" -> ") .append(indexed(fieldPrefix, actualFieldIndex())); } sb.append(":"); if (isMessage()) { sb.append("\n"); printChildContents(includeMatches, indexed(fieldPrefix, actualFieldIndex()), sb); } else { sb.append(" "); printFieldValue(fieldDescriptor(), expected().get(), sb); sb.append(" -> "); printFieldValue(fieldDescriptor(), actual().get(), sb); } return; case REMOVED: sb.append("deleted: ").append(indexed(fieldPrefix, expectedFieldIndex())).append(": "); if (isMessage()) { sb.append("\n"); printMessage((Message) expected().get(), sb); } else { printFieldValue(fieldDescriptor(), expected().get(), sb); sb.append("\n"); } return; } throw new AssertionError("Impossible: " + result()); } @Override final boolean isContentEmpty() { return false; } abstract Builder toBuilder(); static Builder newBuilder() { return new AutoValue_DiffResult_RepeatedField_PairResult.Builder(); } @AutoValue.Builder abstract static class Builder { abstract Builder setResult(Result result); abstract Builder setFieldDescriptor(FieldDescriptor fieldDescriptor); abstract Builder setActualFieldIndex(int actualFieldIndex); abstract Builder setExpectedFieldIndex(int expectedFieldIndex); abstract Builder setActual(Object actual); abstract Builder setExpected(Object expected); abstract Builder setProtoPrinter(TextFormat.Printer value); abstract Builder setBreakdown(DiffResult breakdown); abstract PairResult build(); } } /** The {@link FieldDescriptor} for this repeated field. */ abstract FieldDescriptor fieldDescriptor(); /** The elements under test. */ abstract ImmutableList actual(); /** The elements expected. */ abstract ImmutableList expected(); // TODO(user,peteg): Also provide a minimum-edit-distance pairing between unmatched elements, // and the diff report between them. /** Pairs of elements which were diffed against each other. */ abstract ImmutableList pairResults(); @Memoized @Override Iterable childEntities() { return pairResults(); } @Override final void printContents(boolean includeMatches, String fieldPrefix, StringBuilder sb) { fieldPrefix = newFieldPrefix(fieldPrefix, fieldDescriptor().getName()); for (PairResult pairResult : pairResults()) { pairResult.printContentsForRepeatedField( /* includeSelfAlways = */ !isMatched(), includeMatches, fieldPrefix, sb); } } @Override final boolean isContentEmpty() { return pairResults().isEmpty(); } static Builder newBuilder() { return new AutoValue_DiffResult_RepeatedField.Builder(); } @AutoValue.Builder abstract static class Builder { abstract Builder setFieldDescriptor(FieldDescriptor fieldDescriptor); abstract Builder setActual(Iterable actual); abstract Builder setExpected(Iterable expected); @ForOverride abstract ImmutableList.Builder pairResultsBuilder(); @CanIgnoreReturnValue final Builder addPairResult(PairResult pairResult) { pairResultsBuilder().add(pairResult); return this; } abstract RepeatedField build(); } } /** Structural summary of the difference between two unknown field sets. */ @AutoValue abstract static class UnknownFieldSetDiff extends RecursableDiffEntity.WithoutResultCode { /** The {@link UnknownFieldSet} being tested. */ abstract Optional actual(); /** The {@link UnknownFieldSet} expected. */ abstract Optional expected(); /** * A list of top-level singular field comparison results. * *

All unknown fields are treated as repeated and with {@code ignoringRepeatedFieldOrder()} * off, because we don't know their nature. If they're optional, only last element matters, but * if they're repeated, all elements matter. */ abstract ImmutableListMultimap singularFields(); @Memoized @Override Iterable childEntities() { return singularFields().values(); } @Override final void printContents(boolean includeMatches, String fieldPrefix, StringBuilder sb) { if (!includeMatches && isMatched()) { return; } for (int fieldNumber : singularFields().keySet()) { for (SingularField singularField : singularFields().get(fieldNumber)) { singularField.printContents(includeMatches, fieldPrefix, sb); } } } @Override final boolean isContentEmpty() { return singularFields().isEmpty(); } static Builder newBuilder() { return new AutoValue_DiffResult_UnknownFieldSetDiff.Builder(); } @AutoValue.Builder abstract static class Builder { abstract Builder setActual(UnknownFieldSet actual); abstract Builder setExpected(UnknownFieldSet expected); @ForOverride abstract ImmutableListMultimap.Builder singularFieldsBuilder(); @CanIgnoreReturnValue final Builder addSingularField(int fieldNumber, SingularField singularField) { singularFieldsBuilder().put(fieldNumber, singularField); return this; } @CanIgnoreReturnValue final Builder addAllSingularFields(int fieldNumber, Iterable singularFields) { singularFieldsBuilder().putAll(fieldNumber, singularFields); return this; } abstract UnknownFieldSetDiff build(); } } /** Utilities to support printing messages and proto fields using a {@link TextFormat.Printer}. */ interface ProtoPrintable { TextFormat.Printer protoPrinter(); default void printMessage(Message m, StringBuilder sb) { try { protoPrinter().print(m, sb); } catch (IOException impossible) { throw new AssertionError(impossible); } } default void printFieldValue(SubScopeId subScopeId, Object o, StringBuilder sb) { switch (subScopeId.kind()) { case FIELD_DESCRIPTOR: printFieldValue(subScopeId.fieldDescriptor(), o, sb); return; case UNKNOWN_FIELD_DESCRIPTOR: printFieldValue(subScopeId.unknownFieldDescriptor(), o, sb); return; case UNPACKED_ANY_VALUE_TYPE: printFieldValue(AnyUtils.valueFieldDescriptor(), o, sb); return; } throw new AssertionError(subScopeId.kind()); } default void printFieldValue(FieldDescriptor field, Object value, StringBuilder sb) { try { protoPrinter().printFieldValue(field, value, sb); } catch (IOException impossible) { throw new AssertionError(impossible); } } default void printFieldValue( UnknownFieldDescriptor unknownField, Object value, StringBuilder sb) { try { TextFormat.printUnknownFieldValue(unknownField.type().wireType(), value, sb); } catch (IOException impossible) { throw new AssertionError(impossible); } } } /** The {@link Message} being tested. */ abstract Message actual(); /** The {@link Message} expected. */ abstract Message expected(); /** A list of top-level singular field comparison results grouped by field number. */ abstract ImmutableListMultimap singularFields(); /** * A list of top-level repeated field comparison results grouped by field number. * *

This is only populated if {@link FluentEqualityConfig#ignoreRepeatedFieldOrder()} is set. * Otherwise, repeated fields are compared strictly in index order, as singular fields. */ abstract ImmutableListMultimap repeatedFields(); /** * The result of comparing the message's {@link UnknownFieldSet}s. Not present if unknown fields * were not compared. */ abstract Optional unknownFields(); @Memoized @Override Iterable childEntities() { // Assemble the diffs in field number order so it most closely matches the schema. ImmutableList.Builder builder = ImmutableList.builderWithExpectedSize( singularFields().size() + repeatedFields().size() + unknownFields().asSet().size()); Set fieldNumbers = Sets.union(singularFields().keySet(), repeatedFields().keySet()); for (int fieldNumber : Ordering.natural().sortedCopy(fieldNumbers)) { builder.addAll(singularFields().get(fieldNumber)); builder.addAll(repeatedFields().get(fieldNumber)); } builder.addAll(unknownFields().asSet()); return builder.build(); } /** Prints the full {@link DiffResult} to a human-readable string, for use in test outputs. */ final String printToString(boolean reportMismatchesOnly) { StringBuilder sb = new StringBuilder(); if (!isMatched()) { sb.append("Differences were found:\n"); printContents(/* includeMatches = */ false, /* fieldPrefix = */ "", sb); if (!reportMismatchesOnly && isAnyChildMatched()) { sb.append("\nFull diff report:\n"); printContents(/* includeMatches = */ true, /* fieldPrefix = */ "", sb); } } else { sb.append("No differences were found."); if (!reportMismatchesOnly) { if (isAnyChildIgnored()) { sb.append("\nSome fields were ignored for comparison, however.\n"); } else { sb.append("\nFull diff report:\n"); } printContents(/* includeMatches = */ true, /* fieldPrefix = */ "", sb); } } return sb.toString(); } @Override final void printContents(boolean includeMatches, String fieldPrefix, StringBuilder sb) { for (RecursableDiffEntity child : childEntities()) { child.printContents(includeMatches, fieldPrefix, sb); } } @Override final boolean isContentEmpty() { return Iterables.isEmpty(childEntities()); } static Builder newBuilder() { return new AutoValue_DiffResult.Builder(); } private static String newFieldPrefix(String rootFieldPrefix, String toAdd) { return rootFieldPrefix.isEmpty() ? toAdd : (rootFieldPrefix + "." + toAdd); } @AutoValue.Builder abstract static class Builder { abstract Builder setActual(Message actual); abstract Builder setExpected(Message expected); @ForOverride abstract ImmutableListMultimap.Builder singularFieldsBuilder(); @CanIgnoreReturnValue final Builder addSingularField(int fieldNumber, SingularField singularField) { singularFieldsBuilder().put(fieldNumber, singularField); return this; } @CanIgnoreReturnValue final Builder addAllSingularFields(int fieldNumber, Iterable singularFields) { singularFieldsBuilder().putAll(fieldNumber, singularFields); return this; } @ForOverride abstract ImmutableListMultimap.Builder repeatedFieldsBuilder(); @CanIgnoreReturnValue final Builder addRepeatedField(int fieldNumber, RepeatedField repeatedField) { repeatedFieldsBuilder().put(fieldNumber, repeatedField); return this; } abstract Builder setUnknownFields(UnknownFieldSetDiff unknownFields); abstract DiffResult build(); } }