diff --git a/api/src/main/java/io/grpc/EquivalentAddressGroup.java b/api/src/main/java/io/grpc/EquivalentAddressGroup.java
index bf8a864902c..2ea257fd35a 100644
--- a/api/src/main/java/io/grpc/EquivalentAddressGroup.java
+++ b/api/src/main/java/io/grpc/EquivalentAddressGroup.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2015 The gRPC Authors
+ * Copyright 2026 The gRPC Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,151 +16,77 @@
package io.grpc;
-import com.google.common.base.Preconditions;
-import java.lang.annotation.Documented;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
+import static com.google.common.truth.Truth.assertThat;
+
+import java.lang.reflect.Field;
import java.net.SocketAddress;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
/**
- * A group of {@link SocketAddress}es that are considered equivalent when channel makes connections.
- *
- *
Usually the addresses are addresses resolved from the same host name, and connecting to any of
- * them is equally sufficient. They do have order. An address appears earlier on the list is likely
- * to be tried earlier.
+ * Unit tests for {@link EquivalentAddressGroup}.
*/
-@ExperimentalApi("https://github.com/grpc/grpc-java/issues/1770")
-public final class EquivalentAddressGroup {
-
- /**
- * The authority to be used when constructing Subchannels for this EquivalentAddressGroup.
- * However, if the channel has overridden authority via
- * {@link ManagedChannelBuilder#overrideAuthority(String)}, the transport will use the channel's
- * authority override.
- *
- *
The authority must be from a trusted source, because if the authority is
- * tampered with, RPCs may be sent to attackers which may leak sensitive user data. If the
- * authority was acquired by doing I/O, the communication must be authenticated (e.g., via TLS).
- * Recognize that the server that provided the authority can trivially impersonate the service.
- */
- @Attr
- @ExperimentalApi("https://github.com/grpc/grpc-java/issues/6138")
- public static final Attributes.Key ATTR_AUTHORITY_OVERRIDE =
- Attributes.Key.create("io.grpc.EquivalentAddressGroup.ATTR_AUTHORITY_OVERRIDE");
- /**
- * The name of the locality that this EquivalentAddressGroup is in.
- */
- public static final Attributes.Key ATTR_LOCALITY_NAME =
- Attributes.Key.create("io.grpc.EquivalentAddressGroup.LOCALITY");
- private final List addrs;
- private final Attributes attrs;
-
- /**
- * {@link SocketAddress} docs say that the addresses are immutable, so we cache the hashCode.
- */
- private final int hashCode;
-
- /**
- * List constructor without {@link Attributes}.
- */
- public EquivalentAddressGroup(List addrs) {
- this(addrs, Attributes.EMPTY);
- }
+@RunWith(JUnit4.class)
+public class EquivalentAddressGroupTest {
- /**
- * List constructor with {@link Attributes}.
- */
- public EquivalentAddressGroup(List addrs, @Attr Attributes attrs) {
- Preconditions.checkArgument(!addrs.isEmpty(), "addrs is empty");
- this.addrs = Collections.unmodifiableList(new ArrayList<>(addrs));
- this.attrs = Preconditions.checkNotNull(attrs, "attrs");
- // Attributes may contain mutable objects, which means Attributes' hashCode may change over
- // time, thus we don't cache Attributes' hashCode.
- hashCode = this.addrs.hashCode();
- }
+ @Test
+ public void toString_summarizesLargeAddressList() {
+ int maxAddressesToString = maxAddressesToString();
+ List addrs = new ArrayList<>();
+ for (int i = 0; i <= maxAddressesToString; i++) {
+ addrs.add(new FakeSocketAddress("addr" + i));
+ }
+ EquivalentAddressGroup eag = new EquivalentAddressGroup(addrs);
- /**
- * Singleton constructor without Attributes.
- */
- public EquivalentAddressGroup(SocketAddress addr) {
- this(addr, Attributes.EMPTY);
+ StringBuilder expected = new StringBuilder();
+ expected.append('[').append('[');
+ for (int i = 0; i < maxAddressesToString; i++) {
+ if (i > 0) {
+ expected.append(", ");
+ }
+ expected.append(addrs.get(i));
+ }
+ expected.append(", ... 1 more]/{}]");
+ assertThat(eag.toString()).isEqualTo(expected.toString());
}
- /**
- * Singleton constructor with Attributes.
- */
- public EquivalentAddressGroup(SocketAddress addr, @Attr Attributes attrs) {
- this(Collections.singletonList(addr), attrs);
- }
+ @Test
+ public void toString_doesNotSummarizeAtMaxAddressCount() {
+ int maxAddressesToString = maxAddressesToString();
+ List addrs = new ArrayList<>();
+ for (int i = 0; i < maxAddressesToString; i++) {
+ addrs.add(new FakeSocketAddress("addr" + i));
+ }
+ EquivalentAddressGroup eag = new EquivalentAddressGroup(addrs);
- /**
- * Returns an immutable list of the addresses.
- */
- public List getAddresses() {
- return addrs;
+ String expected = "[" + addrs + "/{}]";
+ assertThat(eag.toString()).isEqualTo(expected);
}
- /**
- * Returns the attributes.
- */
- @Attr
- public Attributes getAttributes() {
- return attrs;
+ private static int maxAddressesToString() {
+ try {
+ Field field = EquivalentAddressGroup.class.getDeclaredField("MAX_ADDRESSES_TO_STRING");
+ field.setAccessible(true);
+ return (int) field.get(null);
+ } catch (NoSuchFieldException | IllegalAccessException e) {
+ throw new LinkageError("Unable to read MAX_ADDRESSES_TO_STRING", e);
+ }
}
- @Override
- public String toString() {
- // TODO(zpencer): Summarize return value if addr is very large
- return "[" + addrs + "/" + attrs + "]";
- }
+ private static final class FakeSocketAddress extends SocketAddress {
- @Override
- public int hashCode() {
- // Avoids creating an iterator on the underlying array list.
- return hashCode;
- }
+ private final String name;
- /**
- * Returns true if the given object is also an {@link EquivalentAddressGroup} with an equal
- * address list and equal attribute values.
- *
- * Note that if the attributes include mutable values, it is possible for two objects to be
- * considered equal at one point in time and not equal at another (due to concurrent mutation of
- * attribute values).
- */
- @Override
- public boolean equals(Object other) {
- if (this == other) {
- return true;
- }
- if (!(other instanceof EquivalentAddressGroup)) {
- return false;
+ FakeSocketAddress(String name) {
+ this.name = name;
}
- EquivalentAddressGroup that = (EquivalentAddressGroup) other;
- if (addrs.size() != that.addrs.size()) {
- return false;
- }
- // Avoids creating an iterator on the underlying array list.
- for (int i = 0; i < addrs.size(); i++) {
- if (!addrs.get(i).equals(that.addrs.get(i))) {
- return false;
- }
- }
- if (!attrs.equals(that.attrs)) {
- return false;
+
+ @Override
+ public String toString() {
+ return name;
}
- return true;
}
-
- /**
- * Annotation for {@link EquivalentAddressGroup}'s attributes. It follows the annotation semantics
- * defined by {@link Attributes}.
- */
- @ExperimentalApi("https://github.com/grpc/grpc-java/issues/4972")
- @Retention(RetentionPolicy.SOURCE)
- @Documented
- public @interface Attr {}
}
diff --git a/api/src/test/java/io/grpc/CallOptionsTest.java b/api/src/test/java/io/grpc/CallOptionsTest.java
index 65fb7ff3bf2..6cdc0f983f7 100644
--- a/api/src/test/java/io/grpc/CallOptionsTest.java
+++ b/api/src/test/java/io/grpc/CallOptionsTest.java
@@ -29,7 +29,9 @@
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
-import com.google.common.base.Objects;
+import com.google.common.truth.Fact;
+import com.google.common.truth.FailureMetadata;
+import com.google.common.truth.Subject;
import io.grpc.ClientStreamTracer.StreamInfo;
import io.grpc.internal.SerializingExecutor;
import java.time.Duration;
@@ -106,17 +108,15 @@ public void allWiths() {
@Test
public void noStrayModifications() {
- assertThat(equal(allSet, allSet.withAuthority("blah").withAuthority(sampleAuthority)))
- .isTrue();
- assertThat(
- equal(allSet,
- allSet.withDeadline(Deadline.after(314, NANOSECONDS)).withDeadline(sampleDeadline)))
- .isTrue();
- assertThat(
- equal(allSet,
+ assertAbout(callOptions()).that(allSet.withAuthority("blah").withAuthority(sampleAuthority))
+ .isEquivalentTo(allSet);
+ assertAbout(callOptions()).that(
+ allSet.withDeadline(Deadline.after(314, NANOSECONDS)).withDeadline(sampleDeadline))
+ .isEquivalentTo(allSet);
+ assertAbout(callOptions()).that(
allSet.withCallCredentials(mock(CallCredentials.class))
- .withCallCredentials(sampleCreds)))
- .isTrue();
+ .withCallCredentials(sampleCreds))
+ .isEquivalentTo(allSet);
}
@Test
@@ -265,12 +265,32 @@ public void getWaitForReady() {
assertSame(CallOptions.DEFAULT.withoutWaitForReady().getWaitForReady(), Boolean.FALSE);
}
- // Only used in noStrayModifications()
- // TODO(carl-mastrangelo): consider making a CallOptionsSubject for Truth.
- private static boolean equal(CallOptions o1, CallOptions o2) {
- return Objects.equal(o1.getDeadline(), o2.getDeadline())
- && Objects.equal(o1.getAuthority(), o2.getAuthority())
- && Objects.equal(o1.getCredentials(), o2.getCredentials());
+ private static Subject.Factory callOptions() {
+ return CallOptionsSubject::new;
+ }
+
+ private static final class CallOptionsSubject extends Subject {
+
+ private final CallOptions actual;
+
+ private CallOptionsSubject(FailureMetadata metadata, CallOptions actual) {
+ super(metadata, actual);
+ this.actual = actual;
+ }
+
+ public void isEquivalentTo(CallOptions expected) {
+ if (actual == null) {
+ failWithActual("expected", expected);
+ return;
+ }
+ if (expected == null) {
+ failWithoutActual(Fact.simpleFact("expected non-null CallOptions"));
+ return;
+ }
+ check("deadline").that(actual.getDeadline()).isEqualTo(expected.getDeadline());
+ check("authority").that(actual.getAuthority()).isEqualTo(expected.getAuthority());
+ check("credentials").that(actual.getCredentials()).isEqualTo(expected.getCredentials());
+ }
}
private static class FakeTicker extends Deadline.Ticker {
diff --git a/api/src/test/java/io/grpc/EquivalentAddressGroupTest.java b/api/src/test/java/io/grpc/EquivalentAddressGroupTest.java
new file mode 100644
index 00000000000..2ea257fd35a
--- /dev/null
+++ b/api/src/test/java/io/grpc/EquivalentAddressGroupTest.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2026 The gRPC Authors
+ *
+ * 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 io.grpc;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import java.lang.reflect.Field;
+import java.net.SocketAddress;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+/**
+ * Unit tests for {@link EquivalentAddressGroup}.
+ */
+@RunWith(JUnit4.class)
+public class EquivalentAddressGroupTest {
+
+ @Test
+ public void toString_summarizesLargeAddressList() {
+ int maxAddressesToString = maxAddressesToString();
+ List addrs = new ArrayList<>();
+ for (int i = 0; i <= maxAddressesToString; i++) {
+ addrs.add(new FakeSocketAddress("addr" + i));
+ }
+ EquivalentAddressGroup eag = new EquivalentAddressGroup(addrs);
+
+ StringBuilder expected = new StringBuilder();
+ expected.append('[').append('[');
+ for (int i = 0; i < maxAddressesToString; i++) {
+ if (i > 0) {
+ expected.append(", ");
+ }
+ expected.append(addrs.get(i));
+ }
+ expected.append(", ... 1 more]/{}]");
+ assertThat(eag.toString()).isEqualTo(expected.toString());
+ }
+
+ @Test
+ public void toString_doesNotSummarizeAtMaxAddressCount() {
+ int maxAddressesToString = maxAddressesToString();
+ List addrs = new ArrayList<>();
+ for (int i = 0; i < maxAddressesToString; i++) {
+ addrs.add(new FakeSocketAddress("addr" + i));
+ }
+ EquivalentAddressGroup eag = new EquivalentAddressGroup(addrs);
+
+ String expected = "[" + addrs + "/{}]";
+ assertThat(eag.toString()).isEqualTo(expected);
+ }
+
+ private static int maxAddressesToString() {
+ try {
+ Field field = EquivalentAddressGroup.class.getDeclaredField("MAX_ADDRESSES_TO_STRING");
+ field.setAccessible(true);
+ return (int) field.get(null);
+ } catch (NoSuchFieldException | IllegalAccessException e) {
+ throw new LinkageError("Unable to read MAX_ADDRESSES_TO_STRING", e);
+ }
+ }
+
+ private static final class FakeSocketAddress extends SocketAddress {
+
+ private final String name;
+
+ FakeSocketAddress(String name) {
+ this.name = name;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+ }
+}