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; + } + } +}