blob: 85dbe548ed9bf2607339489b3a9ad75a52a5b188 [file] [log] [blame]
/*
* 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 static com.google.common.base.Preconditions.checkState;
import static com.google.common.truth.extensions.proto.FieldScopeUtil.join;
import com.google.auto.value.AutoValue;
import com.google.common.base.Function;
import com.google.common.base.Functions;
import com.google.common.base.Optional;
import com.google.common.base.Verify;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.truth.Correspondence;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import com.google.errorprone.annotations.CheckReturnValue;
import com.google.protobuf.Descriptors.Descriptor;
import com.google.protobuf.Descriptors.FieldDescriptor;
import com.google.protobuf.ExtensionRegistry;
import com.google.protobuf.Message;
import com.google.protobuf.TypeRegistry;
import org.checkerframework.checker.nullness.qual.Nullable;
/**
* A specification for a {@link ProtoTruthMessageDifferencer} for comparing two individual
* protobufs.
*
* <p>Can be used to compare lists, maps, and multimaps of protos as well by conversion to a {@link
* Correspondence}.
*/
@AutoValue
abstract class FluentEqualityConfig implements FieldScopeLogicContainer<FluentEqualityConfig> {
private static final FluentEqualityConfig DEFAULT_INSTANCE =
new AutoValue_FluentEqualityConfig.Builder()
.setIgnoreFieldAbsenceScope(FieldScopeLogic.none())
.setIgnoreRepeatedFieldOrderScope(FieldScopeLogic.none())
.setIgnoreExtraRepeatedFieldElementsScope(FieldScopeLogic.none())
.setDoubleCorrespondenceMap(FieldScopeLogicMap.<Correspondence<Number, Number>>empty())
.setFloatCorrespondenceMap(FieldScopeLogicMap.<Correspondence<Number, Number>>empty())
.setCompareExpectedFieldsOnly(false)
.setHasExpectedMessages(false)
.setCompareFieldsScope(FieldScopeLogic.all())
.setReportMismatchesOnly(false)
.setUnpackingAnyUsing(AnyUtils.defaultTypeRegistry(), AnyUtils.defaultExtensionRegistry())
.setUsingCorrespondenceStringFunction(Functions.constant(""))
.build();
static FluentEqualityConfig defaultInstance() {
return DEFAULT_INSTANCE;
}
private final LoadingCache<Descriptor, ProtoTruthMessageDifferencer> messageDifferencers =
CacheBuilder.newBuilder()
.build(
new CacheLoader<Descriptor, ProtoTruthMessageDifferencer>() {
@Override
public ProtoTruthMessageDifferencer load(Descriptor descriptor) {
return ProtoTruthMessageDifferencer.create(FluentEqualityConfig.this, descriptor);
}
});
//////////////////////////////////////////////////////////////////////////////////////////////////
// Storage of AbstractProtoFluentEquals configuration data.
//////////////////////////////////////////////////////////////////////////////////////////////////
abstract FieldScopeLogic ignoreFieldAbsenceScope();
abstract FieldScopeLogic ignoreRepeatedFieldOrderScope();
abstract FieldScopeLogic ignoreExtraRepeatedFieldElementsScope();
abstract FieldScopeLogicMap<Correspondence<Number, Number>> doubleCorrespondenceMap();
abstract FieldScopeLogicMap<Correspondence<Number, Number>> floatCorrespondenceMap();
abstract boolean compareExpectedFieldsOnly();
// Whether 'withExpectedMessages()' has been invoked. This is a book-keeping boolean to ensure
// that 'compareExpectedFieldsOnly()' functions properly; we check that the expected messages are
// provided before we do any diffing, as an internal sanity check.
abstract boolean hasExpectedMessages();
abstract FieldScopeLogic compareFieldsScope();
abstract boolean reportMismatchesOnly();
abstract TypeRegistry useTypeRegistry();
abstract ExtensionRegistry useExtensionRegistry();
// For pretty-printing, does not affect behavior.
abstract Function<? super Optional<Descriptor>, String> usingCorrespondenceStringFunction();
final String usingCorrespondenceString(Optional<Descriptor> descriptor) {
return usingCorrespondenceStringFunction().apply(descriptor);
}
//////////////////////////////////////////////////////////////////////////////////////////////////
// Mutators of FluentEqualityConfig configuration data.
//////////////////////////////////////////////////////////////////////////////////////////////////
final FluentEqualityConfig ignoringFieldAbsence() {
return toBuilder()
.setIgnoreFieldAbsenceScope(FieldScopeLogic.all())
.addUsingCorrespondenceString(".ignoringFieldAbsence()")
.build();
}
final FluentEqualityConfig ignoringFieldAbsenceOfFields(Iterable<Integer> fieldNumbers) {
return toBuilder()
.setIgnoreFieldAbsenceScope(
ignoreFieldAbsenceScope().allowingFieldsNonRecursive(fieldNumbers))
.addUsingCorrespondenceFieldNumbersString(".ignoringFieldAbsenceOf(%s)", fieldNumbers)
.build();
}
final FluentEqualityConfig ignoringFieldAbsenceOfFieldDescriptors(
Iterable<FieldDescriptor> fieldDescriptors) {
return toBuilder()
.setIgnoreFieldAbsenceScope(
ignoreFieldAbsenceScope().allowingFieldDescriptorsNonRecursive(fieldDescriptors))
.addUsingCorrespondenceFieldDescriptorsString(
".ignoringFieldAbsenceOf(%s)", fieldDescriptors)
.build();
}
final FluentEqualityConfig ignoringRepeatedFieldOrder() {
return toBuilder()
.setIgnoreRepeatedFieldOrderScope(FieldScopeLogic.all())
.addUsingCorrespondenceString(".ignoringRepeatedFieldOrder()")
.build();
}
final FluentEqualityConfig ignoringRepeatedFieldOrderOfFields(Iterable<Integer> fieldNumbers) {
return toBuilder()
.setIgnoreRepeatedFieldOrderScope(
ignoreRepeatedFieldOrderScope().allowingFieldsNonRecursive(fieldNumbers))
.addUsingCorrespondenceFieldNumbersString(".ignoringRepeatedFieldOrderOf(%s)", fieldNumbers)
.build();
}
final FluentEqualityConfig ignoringRepeatedFieldOrderOfFieldDescriptors(
Iterable<FieldDescriptor> fieldDescriptors) {
return toBuilder()
.setIgnoreRepeatedFieldOrderScope(
ignoreRepeatedFieldOrderScope().allowingFieldDescriptorsNonRecursive(fieldDescriptors))
.addUsingCorrespondenceFieldDescriptorsString(
".ignoringRepeatedFieldOrderOf(%s)", fieldDescriptors)
.build();
}
final FluentEqualityConfig ignoringExtraRepeatedFieldElements() {
return toBuilder()
.setIgnoreExtraRepeatedFieldElementsScope(FieldScopeLogic.all())
.addUsingCorrespondenceString(".ignoringExtraRepeatedFieldElements()")
.build();
}
final FluentEqualityConfig ignoringExtraRepeatedFieldElementsOfFields(
Iterable<Integer> fieldNumbers) {
return toBuilder()
.setIgnoreExtraRepeatedFieldElementsScope(
ignoreExtraRepeatedFieldElementsScope().allowingFieldsNonRecursive(fieldNumbers))
.addUsingCorrespondenceFieldNumbersString(
".ignoringExtraRepeatedFieldElements(%s)", fieldNumbers)
.build();
}
final FluentEqualityConfig ignoringExtraRepeatedFieldElementsOfFieldDescriptors(
Iterable<FieldDescriptor> fieldDescriptors) {
return toBuilder()
.setIgnoreExtraRepeatedFieldElementsScope(
ignoreExtraRepeatedFieldElementsScope()
.allowingFieldDescriptorsNonRecursive(fieldDescriptors))
.addUsingCorrespondenceFieldDescriptorsString(
".ignoringExtraRepeatedFieldElements(%s)", fieldDescriptors)
.build();
}
final FluentEqualityConfig usingDoubleTolerance(double tolerance) {
return toBuilder()
.setDoubleCorrespondenceMap(
FieldScopeLogicMap.defaultValue(Correspondence.tolerance(tolerance)))
.addUsingCorrespondenceString(".usingDoubleTolerance(" + tolerance + ")")
.build();
}
final FluentEqualityConfig usingDoubleToleranceForFields(
double tolerance, Iterable<Integer> fieldNumbers) {
return toBuilder()
.setDoubleCorrespondenceMap(
doubleCorrespondenceMap()
.with(
FieldScopeLogic.none().allowingFieldsNonRecursive(fieldNumbers),
Correspondence.tolerance(tolerance)))
.addUsingCorrespondenceFieldNumbersString(
".usingDoubleTolerance(" + tolerance + ", %s)", fieldNumbers)
.build();
}
final FluentEqualityConfig usingDoubleToleranceForFieldDescriptors(
double tolerance, Iterable<FieldDescriptor> fieldDescriptors) {
return toBuilder()
.setDoubleCorrespondenceMap(
doubleCorrespondenceMap()
.with(
FieldScopeLogic.none().allowingFieldDescriptorsNonRecursive(fieldDescriptors),
Correspondence.tolerance(tolerance)))
.addUsingCorrespondenceFieldDescriptorsString(
".usingDoubleTolerance(" + tolerance + ", %s)", fieldDescriptors)
.build();
}
final FluentEqualityConfig usingFloatTolerance(float tolerance) {
return toBuilder()
.setFloatCorrespondenceMap(
FieldScopeLogicMap.defaultValue(Correspondence.tolerance(tolerance)))
.addUsingCorrespondenceString(".usingFloatTolerance(" + tolerance + ")")
.build();
}
final FluentEqualityConfig usingFloatToleranceForFields(
float tolerance, Iterable<Integer> fieldNumbers) {
return toBuilder()
.setFloatCorrespondenceMap(
floatCorrespondenceMap()
.with(
FieldScopeLogic.none().allowingFieldsNonRecursive(fieldNumbers),
Correspondence.tolerance(tolerance)))
.addUsingCorrespondenceFieldNumbersString(
".usingFloatTolerance(" + tolerance + ", %s)", fieldNumbers)
.build();
}
final FluentEqualityConfig usingFloatToleranceForFieldDescriptors(
float tolerance, Iterable<FieldDescriptor> fieldDescriptors) {
return toBuilder()
.setFloatCorrespondenceMap(
floatCorrespondenceMap()
.with(
FieldScopeLogic.none().allowingFieldDescriptorsNonRecursive(fieldDescriptors),
Correspondence.tolerance(tolerance)))
.addUsingCorrespondenceFieldDescriptorsString(
".usingFloatTolerance(" + tolerance + ", %s)", fieldDescriptors)
.build();
}
final FluentEqualityConfig comparingExpectedFieldsOnly() {
return toBuilder()
.setCompareExpectedFieldsOnly(true)
.addUsingCorrespondenceString(".comparingExpectedFieldsOnly()")
.build();
}
final FluentEqualityConfig withExpectedMessages(Iterable<? extends Message> messages) {
Builder builder = toBuilder().setHasExpectedMessages(true);
if (compareExpectedFieldsOnly()) {
builder.setCompareFieldsScope(
FieldScopeLogic.and(
compareFieldsScope(),
FieldScopeImpl.createFromSetFields(
messages, useTypeRegistry(), useExtensionRegistry())
.logic()));
}
return builder.build();
}
final FluentEqualityConfig withPartialScope(FieldScope partialScope) {
return toBuilder()
.setCompareFieldsScope(FieldScopeLogic.and(compareFieldsScope(), partialScope.logic()))
.addUsingCorrespondenceFieldScopeString(".withPartialScope(%s)", partialScope)
.build();
}
final FluentEqualityConfig ignoringFields(Iterable<Integer> fieldNumbers) {
return toBuilder()
.setCompareFieldsScope(compareFieldsScope().ignoringFields(fieldNumbers))
.addUsingCorrespondenceFieldNumbersString(".ignoringFields(%s)", fieldNumbers)
.build();
}
final FluentEqualityConfig ignoringFieldDescriptors(Iterable<FieldDescriptor> fieldDescriptors) {
return toBuilder()
.setCompareFieldsScope(compareFieldsScope().ignoringFieldDescriptors(fieldDescriptors))
.addUsingCorrespondenceFieldDescriptorsString(
".ignoringFieldDescriptors(%s)", fieldDescriptors)
.build();
}
final FluentEqualityConfig ignoringFieldScope(FieldScope fieldScope) {
return toBuilder()
.setCompareFieldsScope(
FieldScopeLogic.and(compareFieldsScope(), FieldScopeLogic.not(fieldScope.logic())))
.addUsingCorrespondenceFieldScopeString(".ignoringFieldScope(%s)", fieldScope)
.build();
}
final FluentEqualityConfig reportingMismatchesOnly() {
return toBuilder()
.setReportMismatchesOnly(true)
.addUsingCorrespondenceString(".reportingMismatchesOnly()")
.build();
}
final FluentEqualityConfig unpackingAnyUsing(
TypeRegistry typeRegistry, ExtensionRegistry extensionRegistry) {
return toBuilder()
.setUnpackingAnyUsing(typeRegistry, extensionRegistry)
.addUsingCorrespondenceString(
".unpackingAnyUsing(" + typeRegistry + ", " + extensionRegistry + ")")
.build();
}
@Override
public final FluentEqualityConfig subScope(Descriptor rootDescriptor, SubScopeId subScopeId) {
return toBuilder()
.setIgnoreFieldAbsenceScope(ignoreFieldAbsenceScope().subScope(rootDescriptor, subScopeId))
.setIgnoreRepeatedFieldOrderScope(
ignoreRepeatedFieldOrderScope().subScope(rootDescriptor, subScopeId))
.setIgnoreExtraRepeatedFieldElementsScope(
ignoreExtraRepeatedFieldElementsScope().subScope(rootDescriptor, subScopeId))
.setDoubleCorrespondenceMap(doubleCorrespondenceMap().subScope(rootDescriptor, subScopeId))
.setFloatCorrespondenceMap(floatCorrespondenceMap().subScope(rootDescriptor, subScopeId))
.setCompareFieldsScope(compareFieldsScope().subScope(rootDescriptor, subScopeId))
.build();
}
@Override
public final void validate(
Descriptor rootDescriptor, FieldDescriptorValidator fieldDescriptorValidator) {
// FluentEqualityConfig should never be validated other than as a root entity.
Verify.verify(fieldDescriptorValidator == FieldDescriptorValidator.ALLOW_ALL);
ignoreFieldAbsenceScope()
.validate(rootDescriptor, FieldDescriptorValidator.IS_FIELD_WITH_ABSENCE);
ignoreRepeatedFieldOrderScope()
.validate(rootDescriptor, FieldDescriptorValidator.IS_FIELD_WITH_ORDER);
ignoreExtraRepeatedFieldElementsScope()
.validate(rootDescriptor, FieldDescriptorValidator.IS_FIELD_WITH_EXTRA_ELEMENTS);
doubleCorrespondenceMap().validate(rootDescriptor, FieldDescriptorValidator.IS_DOUBLE_FIELD);
floatCorrespondenceMap().validate(rootDescriptor, FieldDescriptorValidator.IS_FLOAT_FIELD);
compareFieldsScope().validate(rootDescriptor, FieldDescriptorValidator.ALLOW_ALL);
}
//////////////////////////////////////////////////////////////////////////////////////////////////
// Converters into comparison utilities.
//////////////////////////////////////////////////////////////////////////////////////////////////
final ProtoTruthMessageDifferencer toMessageDifferencer(Descriptor descriptor) {
checkState(hasExpectedMessages(), "withExpectedMessages() not called");
return messageDifferencers.getUnchecked(descriptor);
}
final <M extends Message> Correspondence<M, M> toCorrespondence(
final Optional<Descriptor> optDescriptor) {
checkState(hasExpectedMessages(), "withExpectedMessages() not called");
return Correspondence.from(
// If we were allowed lambdas, this would be:
// (M a, M e) ->
// ProtoTruth.assertThat(a).usingConfig(FluentEqualityConfig.this).testIsEqualTo(e),
new Correspondence.BinaryPredicate<M, M>() {
@Override
public boolean apply(@Nullable M actual, @Nullable M expected) {
return ProtoTruth.assertThat(actual)
.usingConfig(FluentEqualityConfig.this)
.testIsEqualTo(expected);
}
},
"is equivalent according to assertThat(proto)"
+ usingCorrespondenceString(optDescriptor)
+ ".isEqualTo(target) to")
.formattingDiffsUsing(
// If we were allowed method references, this would be this::formatDiff.
new Correspondence.DiffFormatter<M, M>() {
@Override
public String formatDiff(@Nullable M actual, @Nullable M expected) {
return FluentEqualityConfig.this.formatDiff(actual, expected);
}
});
}
private <M extends Message> String formatDiff(@Nullable M actual, @Nullable M expected) {
if (actual == null || expected == null) {
return "";
}
return toMessageDifferencer(actual.getDescriptorForType())
.diffMessages(actual, expected)
.printToString(reportMismatchesOnly());
}
//////////////////////////////////////////////////////////////////////////////////////////////////
// Builder methods.
//////////////////////////////////////////////////////////////////////////////////////////////////
abstract Builder toBuilder();
@CanIgnoreReturnValue
@AutoValue.Builder
abstract static class Builder {
abstract Builder setIgnoreFieldAbsenceScope(FieldScopeLogic fieldScopeLogic);
abstract Builder setIgnoreRepeatedFieldOrderScope(FieldScopeLogic fieldScopeLogic);
abstract Builder setIgnoreExtraRepeatedFieldElementsScope(FieldScopeLogic fieldScopeLogic);
abstract Builder setDoubleCorrespondenceMap(
FieldScopeLogicMap<Correspondence<Number, Number>> doubleCorrespondenceMap);
abstract Builder setFloatCorrespondenceMap(
FieldScopeLogicMap<Correspondence<Number, Number>> floatCorrespondenceMap);
abstract Builder setCompareExpectedFieldsOnly(boolean compare);
abstract Builder setHasExpectedMessages(boolean hasExpectedMessages);
abstract Builder setCompareFieldsScope(FieldScopeLogic fieldScopeLogic);
abstract Builder setReportMismatchesOnly(boolean reportMismatchesOnly);
final Builder setUnpackingAnyUsing(
TypeRegistry typeRegistry, ExtensionRegistry extensionRegistry) {
setUseTypeRegistry(typeRegistry);
setUseExtensionRegistry(extensionRegistry);
return this;
}
abstract Builder setUseTypeRegistry(TypeRegistry typeRegistry);
abstract Builder setUseExtensionRegistry(ExtensionRegistry extensionRegistry);
@CheckReturnValue
abstract Function<? super Optional<Descriptor>, String> usingCorrespondenceStringFunction();
abstract Builder setUsingCorrespondenceStringFunction(
Function<? super Optional<Descriptor>, String> usingCorrespondenceStringFunction);
abstract FluentEqualityConfig build();
// Lazy formatting methods.
// These allow us to print raw integer field numbers with meaningful names.
final Builder addUsingCorrespondenceString(String string) {
return setUsingCorrespondenceStringFunction(
FieldScopeUtil.concat(usingCorrespondenceStringFunction(), Functions.constant(string)));
}
final Builder addUsingCorrespondenceFieldNumbersString(
String fmt, Iterable<Integer> fieldNumbers) {
return setUsingCorrespondenceStringFunction(
FieldScopeUtil.concat(
usingCorrespondenceStringFunction(),
FieldScopeUtil.fieldNumbersFunction(fmt, fieldNumbers)));
}
final Builder addUsingCorrespondenceFieldDescriptorsString(
String fmt, Iterable<FieldDescriptor> fieldDescriptors) {
return setUsingCorrespondenceStringFunction(
FieldScopeUtil.concat(
usingCorrespondenceStringFunction(),
Functions.constant(String.format(fmt, join(fieldDescriptors)))));
}
final Builder addUsingCorrespondenceFieldScopeString(String fmt, FieldScope fieldScope) {
return setUsingCorrespondenceStringFunction(
FieldScopeUtil.concat(
usingCorrespondenceStringFunction(),
FieldScopeUtil.fieldScopeFunction(fmt, fieldScope)));
}
}
}