Repository: grpc/grpc-go
File: test/clientconn_test.go (Go)

Code before:
	defer clientConn.Close()

	// Call an RPC to trigger waitForResolvedAddrs.
	ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
	defer cancel()
	client := testgrpc.NewTestServiceClient(clientConn)
	// First RPC call should succeed immediately.
	if _, err := client.EmptyCall(ctx, &testgrpc.Empty{}); err != nil {
		t.Fatalf("First RPC failed unexpectedly: %v", err)
	}
	// Ensure that resolution was not delayed.
	if statsHandler.nameResolutionDelayed {
		t.Fatalf("Expected no name resolution delay, but it was detected.")
	}
}

// TestStatsHandlerDetectsResolutionDelay verifies that the StatsHandler
// detects delays in name resolution when using a manual resolver.
// Ensures that once resolution is updated, the RPC completes successfully.
// Checks that the StatsHandler correctly detects the name resolution delay.
func (s) TestStatsHandlerDetectsResolutionDelay(t *testing.T) {
	stub, cleanup := startStubServer(t)
	defer cleanup()

	statsHandler := &testStatsHandler{}
	clientConn, resolverBuilder := createTestClient(t, "delayed", statsHandler)
	defer clientConn.Close()

	client := testgrpc.NewTestServiceClient(clientConn)
	resolutionReady := make(chan struct{})
	rpcCompleted := make(chan error, 1)
	go func() {
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancel()
		_, err := client.EmptyCall(ctx, &testpb.Empty{})
		rpcCompleted <- err
	}()
	// Simulate delayed resolution and unblock it via resolutionReady
	go func() {
		<-resolutionReady
		resolverBuilder.UpdateState(resolver.State{Addresses: []resolver.Address{{Addr: stub.Address}}})
		t.Log("Resolver state updated, unblocking RPC.")
	}()
	// Block until we’re ready to test the resolution delay
	select {
	case <-time.After(100 * time.Millisecond):
		t.Log("Initial delay passed, signaling resolution to proceed.")
		close(resolutionReady)
	case <-rpcCompleted:
		t.Fatal("RPC completed prematurely before resolution was updated!")
	case <-time.After(5 * time.Second):

Review comment (suggestion):
this can be `cc, rb := createTestClient(t, "delayed", statsHandler)`

Code after:

	ss := &stubserver.StubServer{
		EmptyCallF: func(context.Context, *testpb.Empty) (*testpb.Empty, error) {
			return &testpb.Empty{}, nil
		},
	}
	if err := ss.Start(nil); err != nil {
		t.Fatalf("Failed to start StubServer: %v", err)
	}
	defer ss.Stop()

	statsHandler := &testStatsHandler{}
	rb := manual.NewBuilderWithScheme("delayed")
	cc, err := grpc.NewClient(rb.Scheme()+":///test.server",
		grpc.WithTransportCredentials(insecure.NewCredentials()),
		grpc.WithResolvers(rb),
		grpc.WithStatsHandler(statsHandler),
	)
	if err != nil {
		t.Fatalf("grpc.NewClient() failed: %v", err)
	}
	defer cc.Close()
	go func() {
		<-resolutionWait.Done()
		rb.UpdateState(resolver.State{Addresses: []resolver.Address{{Addr: ss.Address}}})
	}()
	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
	defer cancel()
	client := testgrpc.NewTestServiceClient(cc)
	if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
		t.Fatalf("EmptyCall RPC failed: %v", err)
	}
	if !statsHandler.nameResolutionDelayed {
		t.Fatalf("statsHandler.nameResolutionDelayed = %v; want true", statsHandler.nameResolutionDelayed)
	}
}