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

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

/*
 * 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.Strings.lenientFormat;
import static com.google.common.truth.Fact.fact;
import static com.google.common.truth.Fact.simpleFact;

import com.google.common.base.Objects;
import com.google.common.truth.FailureMetadata;
import com.google.common.truth.IntegerSubject;
import com.google.common.truth.Subject;
import com.google.errorprone.annotations.CheckReturnValue;
import com.google.protobuf.MessageLite;
import java.util.regex.Pattern;
import org.jspecify.annotations.Nullable;

/**
 * Truth subjects for the Lite version of Protocol Buffers.
 *
 * 

LiteProtoSubject supports versions 2 and 3 of Protocol Buffers. Due to the lack of runtime * descriptors, its functionality is limited compared to ProtoSubject, in particular in performing * detailed comparisons between messages. */ @CheckReturnValue public class LiteProtoSubject extends Subject { /** * Returns a {@code Subject.Factory} for {@link MessageLite} subjects which you can use to assert * things about Lite Protobuf properties. */ static Factory liteProtos() { return LiteProtoSubjectFactory.INSTANCE; } /* * Storing a FailureMetadata instance in a Subject subclass is generally a bad practice: The * FailureMetadata instance contains the chain of subjects used to create this one (as in * assertThat(value).hasFooThat().hasBarThat()). Reusing that instance for a different Subject * (like one produced by a hasBazThat() method) would result in a Subject with the wrong chain, * potentially producing misleading failure messages. To get the messages right, Subject * subclasses should instead use check(...). * * However... here we are using the FailureMetadata instance only to create a Subject for an * "equivalent" object. Thus, it will still be accurate for the failure message to be written as * if it's talking about the proto itself. * * TODO(b/127819891): Use a better API for this if one is addded. */ private final FailureMetadata metadata; private final MessageLite actual; protected LiteProtoSubject(FailureMetadata failureMetadata, @Nullable MessageLite messageLite) { super(failureMetadata, messageLite); this.metadata = failureMetadata; this.actual = messageLite; } // It is wrong to compare protos using their string representations. The MessageLite runtime // deliberately prefixes debug strings with their Object.toString() to discourage string // comparison. However, this reads poorly in tests, and makes it harder to identify differences // from the strings alone. So, we manually strip this prefix. // In case the class names are actually relevant, Subject.isEqualTo() will add them back for us. // TODO(user): Maybe get a way to do this upstream. static String getTrimmedToString(@Nullable MessageLite messageLite) { String subjectString = String.valueOf(messageLite); String trimmedSubjectString = subjectString.trim(); if (trimmedSubjectString.startsWith("# ")) { String objectToString = String.format( "# %s@%s", messageLite.getClass().getName(), Integer.toHexString(messageLite.hashCode())); if (trimmedSubjectString.startsWith(objectToString)) { subjectString = trimmedSubjectString.replaceFirst(Pattern.quote(objectToString), "").trim(); } } return subjectString.isEmpty() ? "[empty proto]" : subjectString; } @Override protected String actualCustomStringRepresentation() { return actualCustomStringRepresentationForProtoPackageMembersToCall(); } final String actualCustomStringRepresentationForProtoPackageMembersToCall() { return getTrimmedToString(actual); } /** * Checks whether the MessageLite is equivalent to the argument, using the standard equals() * implementation. */ @Override public void isEqualTo(@Nullable Object expected) { // TODO(user): Do better here when MessageLite descriptors are available. if (Objects.equal(actual, expected)) { return; } if (actual == null || expected == null) { super.isEqualTo(expected); } else if (actual.getClass() != expected.getClass()) { failWithoutActual( simpleFact( lenientFormat( "Not true that (%s) proto is equal to the expected (%s) object. " + "They are not of the same class.", actual.getClass().getName(), expected.getClass().getName()))); } else { /* * TODO(cpovirk): If we someday let subjects override formatActualOrExpected(), change this * class to do so, and make this code path always delegate to super.isEqualTo(). */ String ourString = getTrimmedToString(actual); String theirString = getTrimmedToString((MessageLite) expected); if (!ourString.equals(theirString)) { new LiteProtoAsStringSubject(metadata, ourString).isEqualTo(theirString); // fails } else { // This will include the Object.toString() headers. super.isEqualTo(expected); } } } /** * @deprecated A Builder can never compare equal to a MessageLite instance. Use {@code build()}, * or {@code buildPartial()} on the argument to get a MessageLite for comparison instead. Or, * if you are passing {@code null}, use {@link #isNull()}. */ /* * TODO(cpovirk): Consider @DoNotCall -- or probably some other static analysis, given the problem * discussed in the rest of this comment. * * The problem: isEqualTo(null) resolves to this overload (since this overload is more specific * than isEqualTo(Object)), so @DoNotCall would break all assertions of that form. * * To address that, we could try also adding something like ` void isEqualTo(NullT)` and hoping that isEqualTo(null) would resolve to * that instead. That would also have the benefit of making isEqualTo(null) not produce a * deprecation warning (though of course people "should" use isNull(): b/17294077). But yuck. * * Given the null issue, maybe we should never have added this overload in the first place, * instead adding static analysis specific to MessageLite-MessageLite.Builder comparisons. (Sadly, * we can't remove it now without breaking binary compatibility.) * * Still, we could add static analysis to produce a compile error for isEqualTo(Builder) this even * today, even without using @DoNotCall. And then we could consider removing @Deprecated to stop * spamming the people who call isEqualTo(null). */ @Deprecated public void isEqualTo(MessageLite.@Nullable Builder builder) { isEqualTo((Object) builder); } private static final class LiteProtoAsStringSubject extends Subject { LiteProtoAsStringSubject(FailureMetadata metadata, @Nullable String actual) { super(metadata, actual); } } @Override public void isNotEqualTo(@Nullable Object expected) { if (Objects.equal(actual, expected)) { if (actual == null) { super.isNotEqualTo(expected); } else { failWithoutActual( simpleFact( lenientFormat( "Not true that protos are different. Both are (%s) <%s>.", actual.getClass().getName(), getTrimmedToString(actual)))); } } } /** * @deprecated A Builder will never compare equal to a MessageLite instance. Use {@code build()}, * or {@code buildPartial()} on the argument to get a MessageLite for comparison instead. Or, * if you are passing {@code null}, use {@link #isNotNull()}. */ // TODO(cpovirk): Consider @DoNotCall or other static analysis. (See isEqualTo(Builder).) @Deprecated public void isNotEqualTo(MessageLite.@Nullable Builder builder) { isNotEqualTo((Object) builder); } /** Checks whether the subject is a {@link MessageLite} with no fields set. */ public void isEqualToDefaultInstance() { if (actual == null) { failWithoutActual( simpleFact( lenientFormat( "Not true that <%s> is a default proto instance. It is null.", actualCustomStringRepresentationForProtoPackageMembersToCall()))); } else if (!actual.equals(actual.getDefaultInstanceForType())) { failWithoutActual( simpleFact( lenientFormat( "Not true that <%s> is a default proto instance. It has set values.", actualCustomStringRepresentationForProtoPackageMembersToCall()))); } } /** Checks whether the subject is not equivalent to a {@link MessageLite} with no fields set. */ public void isNotEqualToDefaultInstance() { if (actual != null && actual.equals(actual.getDefaultInstanceForType())) { failWithoutActual( simpleFact( lenientFormat( "Not true that (%s) <%s> is not a default proto instance. It has no set values.", actual.getClass().getName(), actualCustomStringRepresentationForProtoPackageMembersToCall()))); } } /** * Checks whether the subject has all required fields set. Cannot fail for a proto built with * {@code build()}, which itself fails if required fields aren't set. */ public void hasAllRequiredFields() { if (!actual.isInitialized()) { // MessageLite doesn't support reflection so this is the best we can do. failWithoutActual( simpleFact("expected to have all required fields set"), fact("but was", actualCustomStringRepresentationForProtoPackageMembersToCall()), simpleFact("(Lite runtime could not determine which fields were missing.)")); } } /** * Returns an {@link IntegerSubject} on the serialized size of the MessageLite. * *

Assertions can then be changed on the serialized size, to support checks such as {@code * assertThat(myProto).serializedSize().isAtLeast(16)}, etc. */ public IntegerSubject serializedSize() { return check("getSerializedSize()").that(actual.getSerializedSize()); } private static final class LiteProtoSubjectFactory implements Factory { private static final LiteProtoSubjectFactory INSTANCE = new LiteProtoSubjectFactory(); @Override public LiteProtoSubject createSubject( FailureMetadata failureMetadata, @Nullable MessageLite messageLite) { return new LiteProtoSubject(failureMetadata, messageLite); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy