Support deep comparison of unpacked Any messages in FieldNumberTree.

RELNOTES=n/a
PiperOrigin-RevId: 369676038
diff --git a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/AnyUtils.java b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/AnyUtils.java
index 34e1e3e..454f50d 100644
--- a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/AnyUtils.java
+++ b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/AnyUtils.java
@@ -69,8 +69,9 @@
     return DEFAULT_EXTENSION_REGISTRY;
   }
 
-  /** Unpack an `Any` proto using the TypeRegistry and ExtensionRegistry on `config`. */
-  static Optional<Message> unpack(Message any, FluentEqualityConfig config) {
+  /** Unpack an `Any` proto using the given TypeRegistry and ExtensionRegistry. */
+  static Optional<Message> unpack(
+      Message any, TypeRegistry typeRegistry, ExtensionRegistry extensionRegistry) {
     Preconditions.checkArgument(
         any.getDescriptorForType().equals(Any.getDescriptor()),
         "Expected type google.protobuf.Any, but was: %s",
@@ -80,13 +81,12 @@
     ByteString value = (ByteString) any.getField(valueFieldDescriptor());
 
     try {
-      Descriptor descriptor = config.useTypeRegistry().getDescriptorForTypeUrl(typeUrl);
+      Descriptor descriptor = typeRegistry.getDescriptorForTypeUrl(typeUrl);
       if (descriptor == null) {
         return Optional.absent();
       }
 
-      Message defaultMessage =
-          DynamicMessage.parseFrom(descriptor, value, config.useExtensionRegistry());
+      Message defaultMessage = DynamicMessage.parseFrom(descriptor, value, extensionRegistry);
       return Optional.of(defaultMessage);
     } catch (InvalidProtocolBufferException e) {
       return Optional.absent();
diff --git a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/DiffResult.java b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/DiffResult.java
index a0ec937..8790e81 100644
--- a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/DiffResult.java
+++ b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/DiffResult.java
@@ -605,6 +605,8 @@
         return valueString(subScopeId.fieldDescriptor(), o);
       case UNKNOWN_FIELD_DESCRIPTOR:
         return valueString(subScopeId.unknownFieldDescriptor(), o);
+      case UNPACKED_ANY_VALUE_TYPE:
+        return valueString(AnyUtils.valueFieldDescriptor(), o);
     }
     throw new AssertionError(subScopeId.kind());
   }
diff --git a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldNumberTree.java b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldNumberTree.java
index d33e2fe..698b9d9 100644
--- a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldNumberTree.java
+++ b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldNumberTree.java
@@ -16,9 +16,12 @@
 
 package com.google.common.truth.extensions.proto;
 
+import com.google.common.base.Optional;
 import com.google.common.collect.Maps;
 import com.google.protobuf.Descriptors.FieldDescriptor;
+import com.google.protobuf.ExtensionRegistry;
 import com.google.protobuf.Message;
+import com.google.protobuf.TypeRegistry;
 import com.google.protobuf.UnknownFieldSet;
 import java.util.List;
 import java.util.Map;
@@ -62,7 +65,8 @@
     return children.containsKey(subScopeId);
   }
 
-  static FieldNumberTree fromMessage(Message message) {
+  static FieldNumberTree fromMessage(
+      Message message, TypeRegistry typeRegistry, ExtensionRegistry extensionRegistry) {
     FieldNumberTree tree = new FieldNumberTree();
 
     // Known fields.
@@ -72,15 +76,25 @@
       FieldNumberTree childTree = new FieldNumberTree();
       tree.children.put(subScopeId, childTree);
 
-      Object fieldValue = knownFieldValues.get(field);
-      if (field.getJavaType() == FieldDescriptor.JavaType.MESSAGE) {
-        if (field.isRepeated()) {
-          List<?> valueList = (List<?>) fieldValue;
-          for (Object value : valueList) {
-            childTree.merge(fromMessage((Message) value));
+      if (field.equals(AnyUtils.valueFieldDescriptor())) {
+        // Handle Any protos specially.
+        Optional<Message> unpackedAny = AnyUtils.unpack(message, typeRegistry, extensionRegistry);
+        if (unpackedAny.isPresent()) {
+          tree.children.put(
+              SubScopeId.ofUnpackedAnyValueType(unpackedAny.get().getDescriptorForType()),
+              fromMessage(unpackedAny.get(), typeRegistry, extensionRegistry));
+        }
+      } else {
+        Object fieldValue = knownFieldValues.get(field);
+        if (field.getJavaType() == FieldDescriptor.JavaType.MESSAGE) {
+          if (field.isRepeated()) {
+            List<?> valueList = (List<?>) fieldValue;
+            for (Object value : valueList) {
+              childTree.merge(fromMessage((Message) value, typeRegistry, extensionRegistry));
+            }
+          } else {
+            childTree.merge(fromMessage((Message) fieldValue, typeRegistry, extensionRegistry));
           }
-        } else {
-          childTree.merge(fromMessage((Message) fieldValue));
         }
       }
     }
@@ -91,11 +105,14 @@
     return tree;
   }
 
-  static FieldNumberTree fromMessages(Iterable<? extends Message> messages) {
+  static FieldNumberTree fromMessages(
+      Iterable<? extends Message> messages,
+      TypeRegistry typeRegistry,
+      ExtensionRegistry extensionRegistry) {
     FieldNumberTree tree = new FieldNumberTree();
     for (Message message : messages) {
       if (message != null) {
-        tree.merge(fromMessage(message));
+        tree.merge(fromMessage(message, typeRegistry, extensionRegistry));
       }
     }
     return tree;
diff --git a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopeImpl.java b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopeImpl.java
index 4acf991..0eadd85 100644
--- a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopeImpl.java
+++ b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopeImpl.java
@@ -28,7 +28,9 @@
 import com.google.common.collect.Lists;
 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 java.util.List;
 
 /**
@@ -62,13 +64,17 @@
   // Instantiation methods.
   //////////////////////////////////////////////////////////////////////////////////////////////////
 
-  static FieldScope createFromSetFields(Message message) {
+  static FieldScope createFromSetFields(
+      Message message, TypeRegistry typeRegistry, ExtensionRegistry extensionRegistry) {
     return create(
-        FieldScopeLogic.partialScope(message),
+        FieldScopeLogic.partialScope(message, typeRegistry, extensionRegistry),
         Functions.constant(String.format("FieldScopes.fromSetFields({%s})", message.toString())));
   }
 
-  static FieldScope createFromSetFields(Iterable<? extends Message> messages) {
+  static FieldScope createFromSetFields(
+      Iterable<? extends Message> messages,
+      TypeRegistry typeRegistry,
+      ExtensionRegistry extensionRegistry) {
     if (emptyOrAllNull(messages)) {
       return create(
           FieldScopeLogic.none(),
@@ -82,7 +88,8 @@
         getDescriptors(messages));
 
     return create(
-        FieldScopeLogic.partialScope(messages, optDescriptor.get()),
+        FieldScopeLogic.partialScope(
+            messages, optDescriptor.get(), typeRegistry, extensionRegistry),
         Functions.constant(String.format("FieldScopes.fromSetFields(%s)", formatList(messages))));
   }
 
diff --git a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopeLogic.java b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopeLogic.java
index 31fd056..dfca1f8 100644
--- a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopeLogic.java
+++ b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopeLogic.java
@@ -28,7 +28,9 @@
 import com.google.errorprone.annotations.ForOverride;
 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 java.util.List;
 
 /**
@@ -267,14 +269,21 @@
     }
   }
 
-  static FieldScopeLogic partialScope(Message message) {
+  static FieldScopeLogic partialScope(
+      Message message, TypeRegistry typeRegistry, ExtensionRegistry extensionRegistry) {
     return new RootPartialScopeLogic(
-        FieldNumberTree.fromMessage(message), message.toString(), message.getDescriptorForType());
+        FieldNumberTree.fromMessage(message, typeRegistry, extensionRegistry),
+        message.toString(),
+        message.getDescriptorForType());
   }
 
-  static FieldScopeLogic partialScope(Iterable<? extends Message> messages, Descriptor descriptor) {
+  static FieldScopeLogic partialScope(
+      Iterable<? extends Message> messages,
+      Descriptor descriptor,
+      TypeRegistry typeRegistry,
+      ExtensionRegistry extensionRegistry) {
     return new RootPartialScopeLogic(
-        FieldNumberTree.fromMessages(messages),
+        FieldNumberTree.fromMessages(messages, typeRegistry, extensionRegistry),
         Joiner.on(", ").useForNull("null").join(messages),
         descriptor);
   }
@@ -304,11 +313,18 @@
 
     @Override
     final FieldScopeResult policyFor(Descriptor rootDescriptor, SubScopeId subScopeId) {
-      if (subScopeId.kind() == SubScopeId.Kind.UNKNOWN_FIELD_DESCRIPTOR) {
-        return FieldScopeResult.EXCLUDED_RECURSIVELY;
+      FieldDescriptor fieldDescriptor = null;
+      switch (subScopeId.kind()) {
+        case FIELD_DESCRIPTOR:
+          fieldDescriptor = subScopeId.fieldDescriptor();
+          break;
+        case UNPACKED_ANY_VALUE_TYPE:
+          fieldDescriptor = AnyUtils.valueFieldDescriptor();
+          break;
+        case UNKNOWN_FIELD_DESCRIPTOR:
+          return FieldScopeResult.EXCLUDED_RECURSIVELY;
       }
 
-      FieldDescriptor fieldDescriptor = subScopeId.fieldDescriptor();
       if (matchesFieldDescriptor(rootDescriptor, fieldDescriptor)) {
         return FieldScopeResult.of(/* included = */ true, isRecursive);
       }
diff --git a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopes.java b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopes.java
index 9b709e5..0ba6b44 100644
--- a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopes.java
+++ b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FieldScopes.java
@@ -19,7 +19,9 @@
 import static com.google.common.truth.extensions.proto.FieldScopeUtil.asList;
 
 import com.google.protobuf.Descriptors.FieldDescriptor;
+import com.google.protobuf.ExtensionRegistry;
 import com.google.protobuf.Message;
+import com.google.protobuf.TypeRegistry;
 
 /** Factory class for {@link FieldScope} instances. */
 public final class FieldScopes {
@@ -66,7 +68,58 @@
   // Alternatively II, add Scope.PARTIAL support to ProtoFluentEquals, but with a different name and
   // explicit documentation that it may cause issues with Proto 3.
   public static FieldScope fromSetFields(Message message) {
-    return FieldScopeImpl.createFromSetFields(message);
+    return fromSetFields(
+        message, AnyUtils.defaultTypeRegistry(), AnyUtils.defaultExtensionRegistry());
+  }
+
+  /**
+   * Returns a {@link FieldScope} which is constrained to precisely those specific field paths that
+   * are explicitly set in the message. Note that, for version 3 protobufs, such a {@link
+   * FieldScope} will omit fields in the provided message which are set to default values.
+   *
+   * <p>This can be used limit the scope of a comparison to a complex set of fields in a very brief
+   * statement. Often, {@code message} is the expected half of a comparison about to be performed.
+   *
+   * <p>Example usage:
+   *
+   * <pre>{@code
+   * Foo actual = Foo.newBuilder().setBar(3).setBaz(4).build();
+   * Foo expected = Foo.newBuilder().setBar(3).setBaz(5).build();
+   * // Fails, because actual.getBaz() != expected.getBaz().
+   * assertThat(actual).isEqualTo(expected);
+   *
+   * Foo scope = Foo.newBuilder().setBar(2).build();
+   * // Succeeds, because only the field 'bar' is compared.
+   * assertThat(actual).withPartialScope(FieldScopes.fromSetFields(scope)).isEqualTo(expected);
+   *
+   * }</pre>
+   *
+   * <p>The returned {@link FieldScope} does not respect repeated field indices nor map keys. For
+   * example, if the provided message sets different field values for different elements of a
+   * repeated field, like so:
+   *
+   * <pre>{@code
+   * sub_message: {
+   *   foo: "foo"
+   * }
+   * sub_message: {
+   *   bar: "bar"
+   * }
+   * }</pre>
+   *
+   * <p>The {@link FieldScope} will contain {@code sub_message.foo} and {@code sub_message.bar} for
+   * *all* repeated {@code sub_messages}, including those beyond index 1.
+   *
+   * <p>If there are {@code google.protobuf.Any} protos anywhere within these messages, they will be
+   * unpacked using the provided {@link TypeRegistry} and {@link ExtensionRegistry} to determine
+   * which fields within them should be compared.
+   *
+   * @see ProtoFluentAssertion#unpackingAnyUsing
+   * @since 1.2
+   */
+  public static FieldScope fromSetFields(
+      Message message, TypeRegistry typeRegistry, ExtensionRegistry extensionRegistry) {
+    return FieldScopeImpl.createFromSetFields(message, typeRegistry, extensionRegistry);
   }
 
   /**
@@ -89,7 +142,29 @@
    * or the {@link FieldScope} for the merge of all the messages. These are equivalent.
    */
   public static FieldScope fromSetFields(Iterable<? extends Message> messages) {
-    return FieldScopeImpl.createFromSetFields(messages);
+    return fromSetFields(
+        messages, AnyUtils.defaultTypeRegistry(), AnyUtils.defaultExtensionRegistry());
+  }
+
+  /**
+   * Creates a {@link FieldScope} covering the fields set in every message in the provided list of
+   * messages, with the same semantics as in {@link #fromSetFields(Message)}.
+   *
+   * <p>This can be thought of as the union of the {@link FieldScope}s for each individual message,
+   * or the {@link FieldScope} for the merge of all the messages. These are equivalent.
+   *
+   * <p>If there are {@code google.protobuf.Any} protos anywhere within these messages, they will be
+   * unpacked using the provided {@link TypeRegistry} and {@link ExtensionRegistry} to determine
+   * which fields within them should be compared.
+   *
+   * @see ProtoFluentAssertion#unpackingAnyUsing
+   * @since 1.2
+   */
+  public static FieldScope fromSetFields(
+      Iterable<? extends Message> messages,
+      TypeRegistry typeRegistry,
+      ExtensionRegistry extensionRegistry) {
+    return FieldScopeImpl.createFromSetFields(messages, typeRegistry, extensionRegistry);
   }
 
   /**
diff --git a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FluentEqualityConfig.java b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FluentEqualityConfig.java
index 1d5fff1..85dbe54 100644
--- a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FluentEqualityConfig.java
+++ b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/FluentEqualityConfig.java
@@ -274,7 +274,11 @@
     Builder builder = toBuilder().setHasExpectedMessages(true);
     if (compareExpectedFieldsOnly()) {
       builder.setCompareFieldsScope(
-          FieldScopeLogic.and(compareFieldsScope(), FieldScopes.fromSetFields(messages).logic()));
+          FieldScopeLogic.and(
+              compareFieldsScope(),
+              FieldScopeImpl.createFromSetFields(
+                      messages, useTypeRegistry(), useExtensionRegistry())
+                  .logic()));
     }
     return builder.build();
   }
diff --git a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/ProtoTruthMessageDifferencer.java b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/ProtoTruthMessageDifferencer.java
index d843a61..e6e7c69 100644
--- a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/ProtoTruthMessageDifferencer.java
+++ b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/ProtoTruthMessageDifferencer.java
@@ -220,8 +220,10 @@
     if (shouldCompareValue == FieldScopeResult.EXCLUDED_RECURSIVELY) {
       valueDiffResult = SingularField.ignored(name(AnyUtils.valueFieldDescriptor()));
     } else {
-      Optional<Message> unpackedActual = AnyUtils.unpack(actual, config);
-      Optional<Message> unpackedExpected = AnyUtils.unpack(expected, config);
+      Optional<Message> unpackedActual =
+          AnyUtils.unpack(actual, config.useTypeRegistry(), config.useExtensionRegistry());
+      Optional<Message> unpackedExpected =
+          AnyUtils.unpack(expected, config.useTypeRegistry(), config.useExtensionRegistry());
       if (unpackedActual.isPresent()
           && unpackedExpected.isPresent()
           && descriptorsMatch(unpackedActual.get(), unpackedExpected.get())) {
@@ -234,7 +236,10 @@
                 shouldCompareValue == FieldScopeResult.EXCLUDED_NONRECURSIVELY,
                 AnyUtils.valueFieldDescriptor(),
                 name(AnyUtils.valueFieldDescriptor()),
-                config.subScope(rootDescriptor, AnyUtils.valueSubScopeId()));
+                config.subScope(
+                    rootDescriptor,
+                    SubScopeId.ofUnpackedAnyValueType(
+                        unpackedActual.get().getDescriptorForType())));
       } else {
         valueDiffResult =
             compareSingularValue(
diff --git a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/SubScopeId.java b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/SubScopeId.java
index 4860969..925c156 100644
--- a/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/SubScopeId.java
+++ b/extensions/proto/src/main/java/com/google/common/truth/extensions/proto/SubScopeId.java
@@ -17,13 +17,15 @@
 package com.google.common.truth.extensions.proto;
 
 import com.google.auto.value.AutoOneOf;
+import com.google.protobuf.Descriptors.Descriptor;
 import com.google.protobuf.Descriptors.FieldDescriptor;
 
 @AutoOneOf(SubScopeId.Kind.class)
 abstract class SubScopeId {
   enum Kind {
     FIELD_DESCRIPTOR,
-    UNKNOWN_FIELD_DESCRIPTOR;
+    UNKNOWN_FIELD_DESCRIPTOR,
+    UNPACKED_ANY_VALUE_TYPE;
   }
 
   abstract Kind kind();
@@ -32,6 +34,8 @@
 
   abstract UnknownFieldDescriptor unknownFieldDescriptor();
 
+  abstract Descriptor unpackedAnyValueType();
+
   /** Returns a short, human-readable version of this identifier. */
   final String shortName() {
     switch (kind()) {
@@ -41,6 +45,8 @@
             : fieldDescriptor().getName();
       case UNKNOWN_FIELD_DESCRIPTOR:
         return String.valueOf(unknownFieldDescriptor().fieldNumber());
+      case UNPACKED_ANY_VALUE_TYPE:
+        return AnyUtils.valueFieldDescriptor().getName();
     }
     throw new AssertionError(kind());
   }
@@ -52,4 +58,8 @@
   static SubScopeId of(UnknownFieldDescriptor unknownFieldDescriptor) {
     return AutoOneOf_SubScopeId.unknownFieldDescriptor(unknownFieldDescriptor);
   }
+
+  static SubScopeId ofUnpackedAnyValueType(Descriptor unpackedAnyValueType) {
+    return AutoOneOf_SubScopeId.unpackedAnyValueType(unpackedAnyValueType);
+  }
 }
diff --git a/extensions/proto/src/test/java/com/google/common/truth/extensions/proto/FieldScopesTest.java b/extensions/proto/src/test/java/com/google/common/truth/extensions/proto/FieldScopesTest.java
index fb0a07d..f99e755 100644
--- a/extensions/proto/src/test/java/com/google/common/truth/extensions/proto/FieldScopesTest.java
+++ b/extensions/proto/src/test/java/com/google/common/truth/extensions/proto/FieldScopesTest.java
@@ -385,6 +385,96 @@
   }
 
   @Test
+  public void testAnyMessageComparingExpectedFieldsOnly() throws Exception {
+
+    String typeUrl =
+        isProto3()
+            ? "type.googleapis.com/com.google.common.truth.extensions.proto.SubTestMessage3"
+            : "type.googleapis.com/com.google.common.truth.extensions.proto.SubTestMessage2";
+
+    Message message = parse("o_any_message { [" + typeUrl + "]: { o_int: 2 } }");
+    Message eqMessage =
+        parse("o_any_message { [" + typeUrl + "]: { o_int: 2 r_string: \"foo\" } }");
+    Message diffMessage =
+        parse("o_any_message { [" + typeUrl + "]: { o_int: 3 r_string: \"bar\" } }");
+
+    expectThat(eqMessage)
+        .unpackingAnyUsing(getTypeRegistry(), getExtensionRegistry())
+        .comparingExpectedFieldsOnly()
+        .isEqualTo(message);
+    expectThat(diffMessage)
+        .unpackingAnyUsing(getTypeRegistry(), getExtensionRegistry())
+        .comparingExpectedFieldsOnly()
+        .isNotEqualTo(message);
+  }
+
+  @Test
+  public void testInvalidAnyMessageComparingExpectedFieldsOnly() throws Exception {
+
+    Message message = parse("o_any_message { type_url: 'invalid-type' value: 'abc123' }");
+    Message eqMessage = parse("o_any_message { type_url: 'invalid-type' value: 'abc123' }");
+    Message diffMessage = parse("o_any_message { type_url: 'invalid-type' value: 'def456' }");
+
+    expectThat(eqMessage)
+        .unpackingAnyUsing(getTypeRegistry(), getExtensionRegistry())
+        .comparingExpectedFieldsOnly()
+        .isEqualTo(message);
+    expectThat(diffMessage)
+        .unpackingAnyUsing(getTypeRegistry(), getExtensionRegistry())
+        .comparingExpectedFieldsOnly()
+        .isNotEqualTo(message);
+  }
+
+  @Test
+  public void testDifferentAnyMessagesComparingExpectedFieldsOnly() throws Exception {
+
+    // 'o_int' and 'o_float' have the same field numbers in both messages. However, to compare
+    // accurately, we incorporate the unpacked Descriptor type into the FieldNumberTree as well to
+    // disambiguate.
+    String typeUrl1 =
+        isProto3()
+            ? "type.googleapis.com/com.google.common.truth.extensions.proto.SubTestMessage3"
+            : "type.googleapis.com/com.google.common.truth.extensions.proto.SubTestMessage2";
+    String typeUrl2 =
+        isProto3()
+            ? "type.googleapis.com/com.google.common.truth.extensions.proto.SubSubTestMessage3"
+            : "type.googleapis.com/com.google.common.truth.extensions.proto.SubSubTestMessage2";
+
+    Message message =
+        parse(
+            "r_any_message { ["
+                + typeUrl1
+                + "]: { o_int: 2 } } r_any_message { ["
+                + typeUrl2
+                + "]: { o_float: 3.1 } }");
+    Message eqMessage =
+        parse(
+            "r_any_message { ["
+                + typeUrl1
+                + "]: { o_int: 2 o_float: 1.9 } } r_any_message { ["
+                + typeUrl2
+                + "]: { o_int: 5 o_float: 3.1 } }");
+    Message diffMessage =
+        parse(
+            "r_any_message { ["
+                + typeUrl1
+                + "]: { o_int: 5 o_float: 3.1 } } r_any_message { ["
+                + typeUrl2
+                + "]: { o_int: 2 o_float: 1.9 } }");
+
+    expectThat(eqMessage)
+        .unpackingAnyUsing(getTypeRegistry(), getExtensionRegistry())
+        .ignoringRepeatedFieldOrder()
+        .comparingExpectedFieldsOnly()
+        .isEqualTo(message);
+    expectThat(diffMessage)
+        .unpackingAnyUsing(getTypeRegistry(), getExtensionRegistry())
+        .ignoringRepeatedFieldOrder()
+        .comparingExpectedFieldsOnly()
+        .isNotEqualTo(message);
+  }
+
+  @Test
   public void testIgnoringAllButOneFieldOfSubMessage() {
     // Consider all of TestMessage, but none of o_sub_test_message, except
     // o_sub_test_message.o_int.