Files
grpc-go/xds/internal/client/lds_test.go
Easwar Swaminathan dc9615bb06 xds: Initial implementation of a client using the v2 API (#3144)
This object will be used by a higher level xdsClient object, which will
provide the watch API used by the xds resolver and balancer
implementations.
2019-11-12 10:31:11 -08:00

271 lines
8.2 KiB
Go

/*
*
* Copyright 2019 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 client
import (
"errors"
"fmt"
"reflect"
"testing"
"time"
xdspb "github.com/envoyproxy/go-control-plane/envoy/api/v2"
"google.golang.org/grpc/xds/internal/client/fakexds"
)
func TestGetRouteConfigNameFromListener(t *testing.T) {
tests := []struct {
name string
lis *xdspb.Listener
wantRoute string
wantErr bool
}{
{
name: "no-apiListener-field",
lis: &xdspb.Listener{},
wantRoute: "",
wantErr: true,
},
{
name: "badly-marshaled-apiListener",
lis: badAPIListener1,
wantRoute: "",
wantErr: true,
},
{
name: "wrong-type-in-apiListener",
lis: badResourceListener,
wantRoute: "",
wantErr: true,
},
{
name: "empty-httpConnMgr-in-apiListener",
lis: listenerWithEmptyHTTPConnMgr,
wantRoute: "",
wantErr: true,
},
{
name: "scopedRoutes-routeConfig-in-apiListener",
lis: listenerWithScopedRoutesRouteConfig,
wantRoute: "",
wantErr: true,
},
{
name: "goodListener1",
lis: goodListener1,
wantRoute: goodRouteName1,
wantErr: false,
},
}
for _, test := range tests {
gotRoute, err := getRouteConfigNameFromListener(test.lis)
if gotRoute != test.wantRoute {
t.Errorf("%s: getRouteConfigNameFromListener(%+v) = %v, want %v", test.name, test.lis, gotRoute, test.wantRoute)
}
if (err != nil) != test.wantErr {
t.Errorf("%s: getRouteConfigNameFromListener(%+v) = %v, want %v", test.name, test.lis, err, test.wantErr)
}
}
}
// TestHandleLDSResponse starts a fake xDS server, makes a ClientConn to it,
// and creates a v2Client using it. Then, it registers a watchLDS and tests
// different LDS responses.
func TestHandleLDSResponse(t *testing.T) {
fakeServer, client, cleanup := fakexds.StartClientAndServer(t)
defer cleanup()
v2c := newV2Client(client, goodNodeProto, func(int) time.Duration { return 0 })
tests := []struct {
name string
ldsResponse *xdspb.DiscoveryResponse
wantErr bool
wantUpdate *ldsUpdate
wantUpdateErr bool
}{
// Badly marshaled LDS response.
{
name: "badly-marshaled-response",
ldsResponse: badlyMarshaledLDSResponse,
wantErr: true,
wantUpdate: nil,
wantUpdateErr: false,
},
// Response does not contain Listener proto.
{
name: "no-listener-proto-in-response",
ldsResponse: badResourceTypeInLDSResponse,
wantErr: true,
wantUpdate: nil,
wantUpdateErr: false,
},
// No APIListener in the response. Just one test case here for a bad
// ApiListener, since the others are covered in
// TestGetRouteConfigNameFromListener.
{
name: "no-apiListener-in-response",
ldsResponse: noAPIListenerLDSResponse,
wantErr: true,
wantUpdate: nil,
wantUpdateErr: false,
},
// Response contains one listener and it is good.
{
name: "one-good-listener",
ldsResponse: goodLDSResponse1,
wantErr: false,
wantUpdate: &ldsUpdate{routeName: goodRouteName1},
wantUpdateErr: false,
},
// Response contains multiple good listeners, including the one we are
// interested in.
{
name: "multiple-good-listener",
ldsResponse: ldsResponseWithMultipleResources,
wantErr: false,
wantUpdate: &ldsUpdate{routeName: goodRouteName1},
wantUpdateErr: false,
},
// Response contains two good listeners (one interesting and one
// uninteresting), and one badly marshaled listener.
{
name: "good-bad-ugly-listeners",
ldsResponse: goodBadUglyLDSResponse,
wantErr: false,
wantUpdate: &ldsUpdate{routeName: goodRouteName1},
wantUpdateErr: false,
},
// Response contains one listener, but we are not interested in it.
{
name: "one-uninteresting-listener",
ldsResponse: goodLDSResponse2,
wantErr: false,
wantUpdate: &ldsUpdate{routeName: ""},
wantUpdateErr: true,
},
// Response constains no resources. This is the case where the server
// does not know about the target we are interested in.
{
name: "empty-response",
ldsResponse: emptyLDSResponse,
wantErr: false,
wantUpdate: &ldsUpdate{routeName: ""},
wantUpdateErr: true,
},
}
for _, test := range tests {
gotUpdateCh := make(chan ldsUpdate, 1)
gotUpdateErrCh := make(chan error, 1)
// Register a watcher, to trigger the v2Client to send an LDS request.
cancelWatch := v2c.watchLDS(goodLDSTarget1, func(u ldsUpdate, err error) {
t.Logf("%s: in v2c.watchLDS callback, ldsUpdate: %+v, err: %v", test.name, u, err)
gotUpdateCh <- u
gotUpdateErrCh <- err
})
// Wait till the request makes it to the fakeServer. This ensures that
// the watch request has been processed by the v2Client.
<-fakeServer.RequestChan
// Directly push the response through a call to handleLDSResponse,
// thereby bypassing the fakeServer.
if err := v2c.handleLDSResponse(test.ldsResponse); (err != nil) != test.wantErr {
t.Fatalf("%s: v2c.handleLDSResponse() returned err: %v, wantErr: %v", test.name, err, test.wantErr)
}
// If the test needs the callback to be invoked, verify the update and
// error pushed to the callback.
if test.wantUpdate != nil {
timer := time.NewTimer(defaultTestTimeout)
select {
case <-timer.C:
t.Fatal("Timeout when expecting LDS update")
case gotUpdate := <-gotUpdateCh:
timer.Stop()
if !reflect.DeepEqual(gotUpdate, *test.wantUpdate) {
t.Fatalf("%s: got LDS update : %+v, want %+v", test.name, gotUpdate, *test.wantUpdate)
}
}
// Since the callback that we registered pushes to both channels at
// the same time, this channel read should return immediately.
gotUpdateErr := <-gotUpdateErrCh
if (gotUpdateErr != nil) != test.wantUpdateErr {
t.Fatalf("%s: got LDS update error {%v}, wantErr: %v", test.name, gotUpdateErr, test.wantUpdateErr)
}
}
cancelWatch()
}
}
// TestHandleLDSResponseWithoutWatch tests the case where the v2Client receives
// an LDS response without a registered watcher.
func TestHandleLDSResponseWithoutWatch(t *testing.T) {
_, client, cleanup := fakexds.StartClientAndServer(t)
defer cleanup()
v2c := newV2Client(client, goodNodeProto, func(int) time.Duration { return 0 })
if v2c.handleLDSResponse(goodLDSResponse1) == nil {
t.Fatal("v2c.handleLDSResponse() succeeded, should have failed")
}
}
// TestLDSWatchExpiryTimer tests the case where the client does not receive an
// LDS response for the request that it sends out. We want the watch callback
// to be invoked with an error once the watchExpiryTimer fires.
func TestLDSWatchExpiryTimer(t *testing.T) {
oldWatchExpiryTimeout := defaultWatchExpiryTimeout
defaultWatchExpiryTimeout = 1 * time.Second
defer func() {
defaultWatchExpiryTimeout = oldWatchExpiryTimeout
}()
fakeServer, client, cleanup := fakexds.StartClientAndServer(t)
defer cleanup()
v2c := newV2Client(client, goodNodeProto, func(int) time.Duration { return 0 })
// Wait till the request makes it to the fakeServer. This ensures that
// the watch request has been processed by the v2Client.
callbackCh := make(chan error, 1)
v2c.watchLDS(goodLDSTarget1, func(u ldsUpdate, err error) {
t.Logf("in v2c.watchLDS callback, ldsUpdate: %+v, err: %v", u, err)
if u.routeName != "" {
callbackCh <- fmt.Errorf("received routeName %v in ldsCallback, wanted empty string", u.routeName)
}
if err == nil {
callbackCh <- errors.New("received nil error in ldsCallback")
}
callbackCh <- nil
})
<-fakeServer.RequestChan
timer := time.NewTimer(2 * time.Second)
select {
case <-timer.C:
t.Fatalf("Timeout expired when expecting LDS update")
case err := <-callbackCh:
timer.Stop()
if err != nil {
t.Fatal(err)
}
}
}