vitess.io/vitess@v0.16.2/go/vt/vtctl/grpcvtctldserver/testutil/util.go (about)

     1  /*
     2  Copyright 2021 The Vitess Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // Package testutil contains utility functions for writing tests for the
    18  // grpcvtctldserver.
    19  package testutil
    20  
    21  import (
    22  	"context"
    23  	"errors"
    24  	"fmt"
    25  	"testing"
    26  
    27  	"google.golang.org/protobuf/proto"
    28  
    29  	"github.com/stretchr/testify/require"
    30  	"golang.org/x/net/nettest"
    31  	"google.golang.org/grpc"
    32  
    33  	"vitess.io/vitess/go/vt/topo"
    34  	"vitess.io/vitess/go/vt/topo/topoproto"
    35  	"vitess.io/vitess/go/vt/vtctl/vtctldclient"
    36  
    37  	topodatapb "vitess.io/vitess/go/vt/proto/topodata"
    38  	vtctldatapb "vitess.io/vitess/go/vt/proto/vtctldata"
    39  	vtctlservicepb "vitess.io/vitess/go/vt/proto/vtctlservice"
    40  )
    41  
    42  // WithTestServer creates a gRPC server listening locally with the given RPC
    43  // implementation, then runs the test func with a client created to point at
    44  // that server.
    45  func WithTestServer(
    46  	t *testing.T,
    47  	server vtctlservicepb.VtctldServer,
    48  	test func(t *testing.T, client vtctldclient.VtctldClient),
    49  ) {
    50  	lis, err := nettest.NewLocalListener("tcp")
    51  	require.NoError(t, err, "cannot create local listener")
    52  
    53  	defer lis.Close()
    54  
    55  	s := grpc.NewServer()
    56  	vtctlservicepb.RegisterVtctldServer(s, server)
    57  
    58  	go s.Serve(lis)
    59  	defer s.Stop()
    60  
    61  	client, err := vtctldclient.New("grpc", lis.Addr().String())
    62  	require.NoError(t, err, "cannot create vtctld client")
    63  	defer client.Close()
    64  
    65  	test(t, client)
    66  }
    67  
    68  // WithTestServers creates N gRPC servers listening locally with the given RPC
    69  // implementations, and then runs the test func with N clients created, where
    70  // clients[i] points at servers[i].
    71  func WithTestServers(
    72  	t *testing.T,
    73  	test func(t *testing.T, clients ...vtctldclient.VtctldClient),
    74  	servers ...vtctlservicepb.VtctldServer,
    75  ) {
    76  	// Declare our recursive helper function so it can refer to itself.
    77  	var withTestServers func(t *testing.T, servers ...vtctlservicepb.VtctldServer)
    78  
    79  	// Preallocate a slice of clients we're eventually going to call the test
    80  	// function with.
    81  	clients := make([]vtctldclient.VtctldClient, 0, len(servers))
    82  
    83  	withTestServers = func(t *testing.T, servers ...vtctlservicepb.VtctldServer) {
    84  		if len(servers) == 0 {
    85  			// We've started up all the test servers and accumulated clients for
    86  			// each of them (or there were no test servers to start, and we've
    87  			// accumulated no clients), so finally we run the test and stop
    88  			// recursing.
    89  			test(t, clients...)
    90  
    91  			return
    92  		}
    93  
    94  		// Start up a test server for the head of our server slice, accumulate
    95  		// the resulting client, and recurse on the tail of our server slice.
    96  		WithTestServer(t, servers[0], func(t *testing.T, client vtctldclient.VtctldClient) {
    97  			clients = append(clients, client)
    98  			withTestServers(t, servers[1:]...)
    99  		})
   100  	}
   101  
   102  	withTestServers(t, servers...)
   103  }
   104  
   105  // AddKeyspace adds a keyspace to a topology, failing a test if that keyspace
   106  // could not be added. It shallow copies the proto struct to prevent XXX_ fields
   107  // from changing in the marshalling.
   108  func AddKeyspace(ctx context.Context, t *testing.T, ts *topo.Server, ks *vtctldatapb.Keyspace) {
   109  	err := ts.CreateKeyspace(ctx, ks.Name, proto.Clone(ks.Keyspace).(*topodatapb.Keyspace))
   110  	require.NoError(t, err)
   111  }
   112  
   113  // AddKeyspaces adds a list of keyspaces to the topology, failing a test if any
   114  // of those keyspaces cannot be added. See AddKeyspace for details.
   115  func AddKeyspaces(ctx context.Context, t *testing.T, ts *topo.Server, keyspaces ...*vtctldatapb.Keyspace) {
   116  	for _, keyspace := range keyspaces {
   117  		AddKeyspace(ctx, t, ts, keyspace)
   118  	}
   119  }
   120  
   121  // AddTabletOptions is a container for different behaviors tests need from
   122  // AddTablet.
   123  type AddTabletOptions struct {
   124  	// AlsoSetShardPrimary is an option to control additional setup to take when
   125  	// AddTablet receives a tablet of type PRIMARY. When set, AddTablet will also
   126  	// update the shard record to make that tablet the primary, and fail the
   127  	// test if the shard record has a serving primary already.
   128  	AlsoSetShardPrimary bool
   129  	// ForceSetShardPrimary, when combined with AlsoSetShardPrimary, will ignore
   130  	// any existing primary in the shard, making the current tablet the serving
   131  	// primary (given it is type PRIMARY), and log that it has done so.
   132  	ForceSetShardPrimary bool
   133  	// SkipShardCreation, when set, makes AddTablet never attempt to create a
   134  	// shard record in the topo under any circumstances.
   135  	SkipShardCreation bool
   136  }
   137  
   138  // AddTablet adds a tablet to the topology, failing a test if that tablet record
   139  // could not be created. It shallow copies to prevent XXX_ fields from changing,
   140  // including nested proto message fields.
   141  //
   142  // AddTablet also optionally adds empty keyspace and shard records to the
   143  // topology, if they are set on the tablet record and they cannot be retrieved
   144  // from the topo server without error.
   145  //
   146  // If AddTablet receives a tablet record with a keyspace and shard set, and that
   147  // tablet's type is PRIMARY, and opts.AlsoSetShardPrimary is set, then AddTablet
   148  // will update the shard record to make that tablet the shard primary and set the
   149  // shard to serving. If that shard record already has a serving primary, then
   150  // AddTablet will fail the test.
   151  func AddTablet(ctx context.Context, t *testing.T, ts *topo.Server, tablet *topodatapb.Tablet, opts *AddTabletOptions) {
   152  	tablet = proto.Clone(tablet).(*topodatapb.Tablet)
   153  	if opts == nil {
   154  		opts = &AddTabletOptions{}
   155  	}
   156  
   157  	err := ts.CreateTablet(ctx, tablet)
   158  	require.NoError(t, err, "CreateTablet(%+v)", tablet)
   159  
   160  	if opts.SkipShardCreation {
   161  		return
   162  	}
   163  
   164  	if tablet.Keyspace != "" {
   165  		if _, err := ts.GetKeyspace(ctx, tablet.Keyspace); err != nil {
   166  			err := ts.CreateKeyspace(ctx, tablet.Keyspace, &topodatapb.Keyspace{})
   167  			require.NoError(t, err, "CreateKeyspace(%s)", tablet.Keyspace)
   168  		}
   169  
   170  		if tablet.Shard != "" {
   171  			if _, err := ts.GetShard(ctx, tablet.Keyspace, tablet.Shard); err != nil {
   172  				err := ts.CreateShard(ctx, tablet.Keyspace, tablet.Shard)
   173  				require.NoError(t, err, "CreateShard(%s, %s)", tablet.Keyspace, tablet.Shard)
   174  			}
   175  
   176  			if tablet.Type == topodatapb.TabletType_PRIMARY && opts.AlsoSetShardPrimary {
   177  				_, err := ts.UpdateShardFields(ctx, tablet.Keyspace, tablet.Shard, func(si *topo.ShardInfo) error {
   178  					if si.IsPrimaryServing && si.PrimaryAlias != nil {
   179  						msg := fmt.Sprintf("shard %v/%v already has a serving primary (%v)", tablet.Keyspace, tablet.Shard, topoproto.TabletAliasString(si.PrimaryAlias))
   180  
   181  						if !opts.ForceSetShardPrimary {
   182  							return errors.New(msg)
   183  						}
   184  
   185  						t.Logf("%s; replacing with %v because ForceSetShardPrimary = true", msg, topoproto.TabletAliasString(tablet.Alias))
   186  					}
   187  
   188  					si.PrimaryAlias = tablet.Alias
   189  					si.IsPrimaryServing = true
   190  					si.PrimaryTermStartTime = tablet.PrimaryTermStartTime
   191  
   192  					return nil
   193  				})
   194  				require.NoError(t, err, "UpdateShardFields(%s, %s) to set %s as serving primary failed", tablet.Keyspace, tablet.Shard, topoproto.TabletAliasString(tablet.Alias))
   195  			}
   196  		}
   197  	}
   198  }
   199  
   200  // AddTablets adds a list of tablets to the topology. See AddTablet for more
   201  // details.
   202  func AddTablets(ctx context.Context, t *testing.T, ts *topo.Server, opts *AddTabletOptions, tablets ...*topodatapb.Tablet) {
   203  	for _, tablet := range tablets {
   204  		AddTablet(ctx, t, ts, tablet, opts)
   205  	}
   206  }
   207  
   208  // AddShards adds a list of shards to the topology, failing a test if any of the
   209  // shard records could not be created. It also ensures that every shard's
   210  // keyspace exists, or creates an empty keyspace if that shard's keyspace does
   211  // not exist.
   212  func AddShards(ctx context.Context, t *testing.T, ts *topo.Server, shards ...*vtctldatapb.Shard) {
   213  	for _, shard := range shards {
   214  		if shard.Keyspace != "" {
   215  			if _, err := ts.GetKeyspace(ctx, shard.Keyspace); err != nil {
   216  				err := ts.CreateKeyspace(ctx, shard.Keyspace, &topodatapb.Keyspace{})
   217  				require.NoError(t, err, "CreateKeyspace(%s)", shard.Keyspace)
   218  			}
   219  		}
   220  
   221  		err := ts.CreateShard(ctx, shard.Keyspace, shard.Name)
   222  		require.NoError(t, err, "CreateShard(%s/%s)", shard.Keyspace, shard.Name)
   223  
   224  		if shard.Shard != nil {
   225  			_, err := ts.UpdateShardFields(ctx, shard.Keyspace, shard.Name, func(si *topo.ShardInfo) error {
   226  				si.Shard = shard.Shard
   227  
   228  				return nil
   229  			})
   230  			require.NoError(t, err, "UpdateShardFields(%s/%s, %v)", shard.Keyspace, shard.Name, shard.Shard)
   231  		}
   232  	}
   233  }
   234  
   235  // SetupReplicationGraphs creates a set of ShardReplication objects in the topo,
   236  // failing the test if any of the records could not be created.
   237  func SetupReplicationGraphs(ctx context.Context, t *testing.T, ts *topo.Server, replicationGraphs ...*topo.ShardReplicationInfo) {
   238  	for _, graph := range replicationGraphs {
   239  		err := ts.UpdateShardReplicationFields(ctx, graph.Cell(), graph.Keyspace(), graph.Shard(), func(sr *topodatapb.ShardReplication) error {
   240  			sr.Nodes = graph.Nodes
   241  			return nil
   242  		})
   243  		require.NoError(t, err, "could not save replication graph for %s/%s in cell %v", graph.Keyspace(), graph.Shard(), graph.Cell())
   244  	}
   245  }
   246  
   247  // UpdateSrvKeyspaces updates a set of SrvKeyspace records, grouped by cell and
   248  // then by keyspace. It fails the test if any records cannot be updated.
   249  func UpdateSrvKeyspaces(ctx context.Context, t *testing.T, ts *topo.Server, srvkeyspacesByCellByKeyspace map[string]map[string]*topodatapb.SrvKeyspace) {
   250  	for cell, srvKeyspacesByKeyspace := range srvkeyspacesByCellByKeyspace {
   251  		for keyspace, srvKeyspace := range srvKeyspacesByKeyspace {
   252  			err := ts.UpdateSrvKeyspace(ctx, cell, keyspace, srvKeyspace)
   253  			require.NoError(t, err, "UpdateSrvKeyspace(%v, %v, %v)", cell, keyspace, srvKeyspace)
   254  		}
   255  	}
   256  }
   257  
   258  func MockGetVersionFromTablet(addrVersionMap map[string]string) func(ta string) (string, error) {
   259  	return func(tabletAddr string) (string, error) {
   260  		return addrVersionMap[tabletAddr], nil
   261  	}
   262  }