go.etcd.io/etcd@v3.3.27+incompatible/etcdserver/membership/cluster_test.go (about)

     1  // Copyright 2015 The etcd Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package membership
    16  
    17  import (
    18  	"encoding/json"
    19  	"fmt"
    20  	"path"
    21  	"reflect"
    22  	"testing"
    23  
    24  	"github.com/coreos/etcd/pkg/mock/mockstore"
    25  	"github.com/coreos/etcd/pkg/testutil"
    26  	"github.com/coreos/etcd/pkg/types"
    27  	"github.com/coreos/etcd/raft/raftpb"
    28  	"github.com/coreos/etcd/store"
    29  )
    30  
    31  func TestClusterMember(t *testing.T) {
    32  	membs := []*Member{
    33  		newTestMember(1, nil, "node1", nil),
    34  		newTestMember(2, nil, "node2", nil),
    35  	}
    36  	tests := []struct {
    37  		id    types.ID
    38  		match bool
    39  	}{
    40  		{1, true},
    41  		{2, true},
    42  		{3, false},
    43  	}
    44  	for i, tt := range tests {
    45  		c := newTestCluster(membs)
    46  		m := c.Member(tt.id)
    47  		if g := m != nil; g != tt.match {
    48  			t.Errorf("#%d: find member = %v, want %v", i, g, tt.match)
    49  		}
    50  		if m != nil && m.ID != tt.id {
    51  			t.Errorf("#%d: id = %x, want %x", i, m.ID, tt.id)
    52  		}
    53  	}
    54  }
    55  
    56  func TestClusterMemberByName(t *testing.T) {
    57  	membs := []*Member{
    58  		newTestMember(1, nil, "node1", nil),
    59  		newTestMember(2, nil, "node2", nil),
    60  	}
    61  	tests := []struct {
    62  		name  string
    63  		match bool
    64  	}{
    65  		{"node1", true},
    66  		{"node2", true},
    67  		{"node3", false},
    68  	}
    69  	for i, tt := range tests {
    70  		c := newTestCluster(membs)
    71  		m := c.MemberByName(tt.name)
    72  		if g := m != nil; g != tt.match {
    73  			t.Errorf("#%d: find member = %v, want %v", i, g, tt.match)
    74  		}
    75  		if m != nil && m.Name != tt.name {
    76  			t.Errorf("#%d: name = %v, want %v", i, m.Name, tt.name)
    77  		}
    78  	}
    79  }
    80  
    81  func TestClusterMemberIDs(t *testing.T) {
    82  	c := newTestCluster([]*Member{
    83  		newTestMember(1, nil, "", nil),
    84  		newTestMember(4, nil, "", nil),
    85  		newTestMember(100, nil, "", nil),
    86  	})
    87  	w := []types.ID{1, 4, 100}
    88  	g := c.MemberIDs()
    89  	if !reflect.DeepEqual(w, g) {
    90  		t.Errorf("IDs = %+v, want %+v", g, w)
    91  	}
    92  }
    93  
    94  func TestClusterPeerURLs(t *testing.T) {
    95  	tests := []struct {
    96  		mems  []*Member
    97  		wurls []string
    98  	}{
    99  		// single peer with a single address
   100  		{
   101  			mems: []*Member{
   102  				newTestMember(1, []string{"http://192.0.2.1"}, "", nil),
   103  			},
   104  			wurls: []string{"http://192.0.2.1"},
   105  		},
   106  
   107  		// single peer with a single address with a port
   108  		{
   109  			mems: []*Member{
   110  				newTestMember(1, []string{"http://192.0.2.1:8001"}, "", nil),
   111  			},
   112  			wurls: []string{"http://192.0.2.1:8001"},
   113  		},
   114  
   115  		// several members explicitly unsorted
   116  		{
   117  			mems: []*Member{
   118  				newTestMember(2, []string{"http://192.0.2.3", "http://192.0.2.4"}, "", nil),
   119  				newTestMember(3, []string{"http://192.0.2.5", "http://192.0.2.6"}, "", nil),
   120  				newTestMember(1, []string{"http://192.0.2.1", "http://192.0.2.2"}, "", nil),
   121  			},
   122  			wurls: []string{"http://192.0.2.1", "http://192.0.2.2", "http://192.0.2.3", "http://192.0.2.4", "http://192.0.2.5", "http://192.0.2.6"},
   123  		},
   124  
   125  		// no members
   126  		{
   127  			mems:  []*Member{},
   128  			wurls: []string{},
   129  		},
   130  
   131  		// peer with no peer urls
   132  		{
   133  			mems: []*Member{
   134  				newTestMember(3, []string{}, "", nil),
   135  			},
   136  			wurls: []string{},
   137  		},
   138  	}
   139  
   140  	for i, tt := range tests {
   141  		c := newTestCluster(tt.mems)
   142  		urls := c.PeerURLs()
   143  		if !reflect.DeepEqual(urls, tt.wurls) {
   144  			t.Errorf("#%d: PeerURLs = %v, want %v", i, urls, tt.wurls)
   145  		}
   146  	}
   147  }
   148  
   149  func TestClusterClientURLs(t *testing.T) {
   150  	tests := []struct {
   151  		mems  []*Member
   152  		wurls []string
   153  	}{
   154  		// single peer with a single address
   155  		{
   156  			mems: []*Member{
   157  				newTestMember(1, nil, "", []string{"http://192.0.2.1"}),
   158  			},
   159  			wurls: []string{"http://192.0.2.1"},
   160  		},
   161  
   162  		// single peer with a single address with a port
   163  		{
   164  			mems: []*Member{
   165  				newTestMember(1, nil, "", []string{"http://192.0.2.1:8001"}),
   166  			},
   167  			wurls: []string{"http://192.0.2.1:8001"},
   168  		},
   169  
   170  		// several members explicitly unsorted
   171  		{
   172  			mems: []*Member{
   173  				newTestMember(2, nil, "", []string{"http://192.0.2.3", "http://192.0.2.4"}),
   174  				newTestMember(3, nil, "", []string{"http://192.0.2.5", "http://192.0.2.6"}),
   175  				newTestMember(1, nil, "", []string{"http://192.0.2.1", "http://192.0.2.2"}),
   176  			},
   177  			wurls: []string{"http://192.0.2.1", "http://192.0.2.2", "http://192.0.2.3", "http://192.0.2.4", "http://192.0.2.5", "http://192.0.2.6"},
   178  		},
   179  
   180  		// no members
   181  		{
   182  			mems:  []*Member{},
   183  			wurls: []string{},
   184  		},
   185  
   186  		// peer with no client urls
   187  		{
   188  			mems: []*Member{
   189  				newTestMember(3, nil, "", []string{}),
   190  			},
   191  			wurls: []string{},
   192  		},
   193  	}
   194  
   195  	for i, tt := range tests {
   196  		c := newTestCluster(tt.mems)
   197  		urls := c.ClientURLs()
   198  		if !reflect.DeepEqual(urls, tt.wurls) {
   199  			t.Errorf("#%d: ClientURLs = %v, want %v", i, urls, tt.wurls)
   200  		}
   201  	}
   202  }
   203  
   204  func TestClusterValidateAndAssignIDsBad(t *testing.T) {
   205  	tests := []struct {
   206  		clmembs []*Member
   207  		membs   []*Member
   208  	}{
   209  		{
   210  			// unmatched length
   211  			[]*Member{
   212  				newTestMember(1, []string{"http://127.0.0.1:2379"}, "", nil),
   213  			},
   214  			[]*Member{},
   215  		},
   216  		{
   217  			// unmatched peer urls
   218  			[]*Member{
   219  				newTestMember(1, []string{"http://127.0.0.1:2379"}, "", nil),
   220  			},
   221  			[]*Member{
   222  				newTestMember(1, []string{"http://127.0.0.1:4001"}, "", nil),
   223  			},
   224  		},
   225  		{
   226  			// unmatched peer urls
   227  			[]*Member{
   228  				newTestMember(1, []string{"http://127.0.0.1:2379"}, "", nil),
   229  				newTestMember(2, []string{"http://127.0.0.2:2379"}, "", nil),
   230  			},
   231  			[]*Member{
   232  				newTestMember(1, []string{"http://127.0.0.1:2379"}, "", nil),
   233  				newTestMember(2, []string{"http://127.0.0.2:4001"}, "", nil),
   234  			},
   235  		},
   236  	}
   237  	for i, tt := range tests {
   238  		ecl := newTestCluster(tt.clmembs)
   239  		lcl := newTestCluster(tt.membs)
   240  		if err := ValidateClusterAndAssignIDs(lcl, ecl); err == nil {
   241  			t.Errorf("#%d: unexpected update success", i)
   242  		}
   243  	}
   244  }
   245  
   246  func TestClusterValidateAndAssignIDs(t *testing.T) {
   247  	tests := []struct {
   248  		clmembs []*Member
   249  		membs   []*Member
   250  		wids    []types.ID
   251  	}{
   252  		{
   253  			[]*Member{
   254  				newTestMember(1, []string{"http://127.0.0.1:2379"}, "", nil),
   255  				newTestMember(2, []string{"http://127.0.0.2:2379"}, "", nil),
   256  			},
   257  			[]*Member{
   258  				newTestMember(3, []string{"http://127.0.0.1:2379"}, "", nil),
   259  				newTestMember(4, []string{"http://127.0.0.2:2379"}, "", nil),
   260  			},
   261  			[]types.ID{3, 4},
   262  		},
   263  	}
   264  	for i, tt := range tests {
   265  		lcl := newTestCluster(tt.clmembs)
   266  		ecl := newTestCluster(tt.membs)
   267  		if err := ValidateClusterAndAssignIDs(lcl, ecl); err != nil {
   268  			t.Errorf("#%d: unexpect update error: %v", i, err)
   269  		}
   270  		if !reflect.DeepEqual(lcl.MemberIDs(), tt.wids) {
   271  			t.Errorf("#%d: ids = %v, want %v", i, lcl.MemberIDs(), tt.wids)
   272  		}
   273  	}
   274  }
   275  
   276  func TestClusterValidateConfigurationChange(t *testing.T) {
   277  	cl := NewCluster("")
   278  	cl.SetStore(store.New())
   279  	for i := 1; i <= 4; i++ {
   280  		attr := RaftAttributes{PeerURLs: []string{fmt.Sprintf("http://127.0.0.1:%d", i)}}
   281  		cl.AddMember(&Member{ID: types.ID(i), RaftAttributes: attr})
   282  	}
   283  	cl.RemoveMember(4)
   284  
   285  	attr := RaftAttributes{PeerURLs: []string{fmt.Sprintf("http://127.0.0.1:%d", 1)}}
   286  	ctx, err := json.Marshal(&Member{ID: types.ID(5), RaftAttributes: attr})
   287  	if err != nil {
   288  		t.Fatal(err)
   289  	}
   290  
   291  	attr = RaftAttributes{PeerURLs: []string{fmt.Sprintf("http://127.0.0.1:%d", 5)}}
   292  	ctx5, err := json.Marshal(&Member{ID: types.ID(5), RaftAttributes: attr})
   293  	if err != nil {
   294  		t.Fatal(err)
   295  	}
   296  
   297  	attr = RaftAttributes{PeerURLs: []string{fmt.Sprintf("http://127.0.0.1:%d", 3)}}
   298  	ctx2to3, err := json.Marshal(&Member{ID: types.ID(2), RaftAttributes: attr})
   299  	if err != nil {
   300  		t.Fatal(err)
   301  	}
   302  
   303  	attr = RaftAttributes{PeerURLs: []string{fmt.Sprintf("http://127.0.0.1:%d", 5)}}
   304  	ctx2to5, err := json.Marshal(&Member{ID: types.ID(2), RaftAttributes: attr})
   305  	if err != nil {
   306  		t.Fatal(err)
   307  	}
   308  
   309  	tests := []struct {
   310  		cc   raftpb.ConfChange
   311  		werr error
   312  	}{
   313  		{
   314  			raftpb.ConfChange{
   315  				Type:   raftpb.ConfChangeRemoveNode,
   316  				NodeID: 3,
   317  			},
   318  			nil,
   319  		},
   320  		{
   321  			raftpb.ConfChange{
   322  				Type:   raftpb.ConfChangeAddNode,
   323  				NodeID: 4,
   324  			},
   325  			ErrIDRemoved,
   326  		},
   327  		{
   328  			raftpb.ConfChange{
   329  				Type:   raftpb.ConfChangeRemoveNode,
   330  				NodeID: 4,
   331  			},
   332  			ErrIDRemoved,
   333  		},
   334  		{
   335  			raftpb.ConfChange{
   336  				Type:   raftpb.ConfChangeAddNode,
   337  				NodeID: 1,
   338  			},
   339  			ErrIDExists,
   340  		},
   341  		{
   342  			raftpb.ConfChange{
   343  				Type:    raftpb.ConfChangeAddNode,
   344  				NodeID:  5,
   345  				Context: ctx,
   346  			},
   347  			ErrPeerURLexists,
   348  		},
   349  		{
   350  			raftpb.ConfChange{
   351  				Type:   raftpb.ConfChangeRemoveNode,
   352  				NodeID: 5,
   353  			},
   354  			ErrIDNotFound,
   355  		},
   356  		{
   357  			raftpb.ConfChange{
   358  				Type:    raftpb.ConfChangeAddNode,
   359  				NodeID:  5,
   360  				Context: ctx5,
   361  			},
   362  			nil,
   363  		},
   364  		{
   365  			raftpb.ConfChange{
   366  				Type:    raftpb.ConfChangeUpdateNode,
   367  				NodeID:  5,
   368  				Context: ctx,
   369  			},
   370  			ErrIDNotFound,
   371  		},
   372  		// try to change the peer url of 2 to the peer url of 3
   373  		{
   374  			raftpb.ConfChange{
   375  				Type:    raftpb.ConfChangeUpdateNode,
   376  				NodeID:  2,
   377  				Context: ctx2to3,
   378  			},
   379  			ErrPeerURLexists,
   380  		},
   381  		{
   382  			raftpb.ConfChange{
   383  				Type:    raftpb.ConfChangeUpdateNode,
   384  				NodeID:  2,
   385  				Context: ctx2to5,
   386  			},
   387  			nil,
   388  		},
   389  	}
   390  	for i, tt := range tests {
   391  		err := cl.ValidateConfigurationChange(tt.cc)
   392  		if err != tt.werr {
   393  			t.Errorf("#%d: validateConfigurationChange error = %v, want %v", i, err, tt.werr)
   394  		}
   395  	}
   396  }
   397  
   398  func TestClusterGenID(t *testing.T) {
   399  	cs := newTestCluster([]*Member{
   400  		newTestMember(1, nil, "", nil),
   401  		newTestMember(2, nil, "", nil),
   402  	})
   403  
   404  	cs.genID()
   405  	if cs.ID() == 0 {
   406  		t.Fatalf("cluster.ID = %v, want not 0", cs.ID())
   407  	}
   408  	previd := cs.ID()
   409  
   410  	cs.SetStore(mockstore.NewNop())
   411  	cs.AddMember(newTestMember(3, nil, "", nil))
   412  	cs.genID()
   413  	if cs.ID() == previd {
   414  		t.Fatalf("cluster.ID = %v, want not %v", cs.ID(), previd)
   415  	}
   416  }
   417  
   418  func TestNodeToMemberBad(t *testing.T) {
   419  	tests := []*store.NodeExtern{
   420  		{Key: "/1234", Nodes: []*store.NodeExtern{
   421  			{Key: "/1234/strange"},
   422  		}},
   423  		{Key: "/1234", Nodes: []*store.NodeExtern{
   424  			{Key: "/1234/raftAttributes", Value: stringp("garbage")},
   425  		}},
   426  		{Key: "/1234", Nodes: []*store.NodeExtern{
   427  			{Key: "/1234/attributes", Value: stringp(`{"name":"node1","clientURLs":null}`)},
   428  		}},
   429  		{Key: "/1234", Nodes: []*store.NodeExtern{
   430  			{Key: "/1234/raftAttributes", Value: stringp(`{"peerURLs":null}`)},
   431  			{Key: "/1234/strange"},
   432  		}},
   433  		{Key: "/1234", Nodes: []*store.NodeExtern{
   434  			{Key: "/1234/raftAttributes", Value: stringp(`{"peerURLs":null}`)},
   435  			{Key: "/1234/attributes", Value: stringp("garbage")},
   436  		}},
   437  		{Key: "/1234", Nodes: []*store.NodeExtern{
   438  			{Key: "/1234/raftAttributes", Value: stringp(`{"peerURLs":null}`)},
   439  			{Key: "/1234/attributes", Value: stringp(`{"name":"node1","clientURLs":null}`)},
   440  			{Key: "/1234/strange"},
   441  		}},
   442  	}
   443  	for i, tt := range tests {
   444  		if _, err := nodeToMember(tt); err == nil {
   445  			t.Errorf("#%d: unexpected nil error", i)
   446  		}
   447  	}
   448  }
   449  
   450  func TestClusterAddMember(t *testing.T) {
   451  	st := mockstore.NewRecorder()
   452  	c := newTestCluster(nil)
   453  	c.SetStore(st)
   454  	c.AddMember(newTestMember(1, nil, "node1", nil))
   455  
   456  	wactions := []testutil.Action{
   457  		{
   458  			Name: "Create",
   459  			Params: []interface{}{
   460  				path.Join(StoreMembersPrefix, "1", "raftAttributes"),
   461  				false,
   462  				`{"peerURLs":null}`,
   463  				false,
   464  				store.TTLOptionSet{ExpireTime: store.Permanent},
   465  			},
   466  		},
   467  	}
   468  	if g := st.Action(); !reflect.DeepEqual(g, wactions) {
   469  		t.Errorf("actions = %v, want %v", g, wactions)
   470  	}
   471  }
   472  
   473  func TestClusterMembers(t *testing.T) {
   474  	cls := &RaftCluster{
   475  		members: map[types.ID]*Member{
   476  			1:   {ID: 1},
   477  			20:  {ID: 20},
   478  			100: {ID: 100},
   479  			5:   {ID: 5},
   480  			50:  {ID: 50},
   481  		},
   482  	}
   483  	w := []*Member{
   484  		{ID: 1},
   485  		{ID: 5},
   486  		{ID: 20},
   487  		{ID: 50},
   488  		{ID: 100},
   489  	}
   490  	if g := cls.Members(); !reflect.DeepEqual(g, w) {
   491  		t.Fatalf("Members()=%#v, want %#v", g, w)
   492  	}
   493  }
   494  
   495  func TestClusterRemoveMember(t *testing.T) {
   496  	st := mockstore.NewRecorder()
   497  	c := newTestCluster(nil)
   498  	c.SetStore(st)
   499  	c.RemoveMember(1)
   500  
   501  	wactions := []testutil.Action{
   502  		{Name: "Delete", Params: []interface{}{MemberStoreKey(1), true, true}},
   503  		{Name: "Create", Params: []interface{}{RemovedMemberStoreKey(1), false, "", false, store.TTLOptionSet{ExpireTime: store.Permanent}}},
   504  	}
   505  	if !reflect.DeepEqual(st.Action(), wactions) {
   506  		t.Errorf("actions = %v, want %v", st.Action(), wactions)
   507  	}
   508  }
   509  
   510  func TestClusterUpdateAttributes(t *testing.T) {
   511  	name := "etcd"
   512  	clientURLs := []string{"http://127.0.0.1:4001"}
   513  	tests := []struct {
   514  		mems    []*Member
   515  		removed map[types.ID]bool
   516  		wmems   []*Member
   517  	}{
   518  		// update attributes of existing member
   519  		{
   520  			[]*Member{
   521  				newTestMember(1, nil, "", nil),
   522  			},
   523  			nil,
   524  			[]*Member{
   525  				newTestMember(1, nil, name, clientURLs),
   526  			},
   527  		},
   528  		// update attributes of removed member
   529  		{
   530  			nil,
   531  			map[types.ID]bool{types.ID(1): true},
   532  			nil,
   533  		},
   534  	}
   535  	for i, tt := range tests {
   536  		c := newTestCluster(tt.mems)
   537  		c.removed = tt.removed
   538  
   539  		c.UpdateAttributes(types.ID(1), Attributes{Name: name, ClientURLs: clientURLs})
   540  		if g := c.Members(); !reflect.DeepEqual(g, tt.wmems) {
   541  			t.Errorf("#%d: members = %+v, want %+v", i, g, tt.wmems)
   542  		}
   543  	}
   544  }
   545  
   546  func TestNodeToMember(t *testing.T) {
   547  	n := &store.NodeExtern{Key: "/1234", Nodes: []*store.NodeExtern{
   548  		{Key: "/1234/attributes", Value: stringp(`{"name":"node1","clientURLs":null}`)},
   549  		{Key: "/1234/raftAttributes", Value: stringp(`{"peerURLs":null}`)},
   550  	}}
   551  	wm := &Member{ID: 0x1234, RaftAttributes: RaftAttributes{}, Attributes: Attributes{Name: "node1"}}
   552  	m, err := nodeToMember(n)
   553  	if err != nil {
   554  		t.Fatalf("unexpected nodeToMember error: %v", err)
   555  	}
   556  	if !reflect.DeepEqual(m, wm) {
   557  		t.Errorf("member = %+v, want %+v", m, wm)
   558  	}
   559  }
   560  
   561  func newTestCluster(membs []*Member) *RaftCluster {
   562  	c := &RaftCluster{members: make(map[types.ID]*Member), removed: make(map[types.ID]bool)}
   563  	for _, m := range membs {
   564  		c.members[m.ID] = m
   565  	}
   566  	return c
   567  }
   568  
   569  func stringp(s string) *string { return &s }
   570  
   571  func TestIsReadyToAddNewMember(t *testing.T) {
   572  	tests := []struct {
   573  		members []*Member
   574  		want    bool
   575  	}{
   576  		{
   577  			// 0/3 members ready, should fail
   578  			[]*Member{
   579  				newTestMember(1, nil, "", nil),
   580  				newTestMember(2, nil, "", nil),
   581  				newTestMember(3, nil, "", nil),
   582  			},
   583  			false,
   584  		},
   585  		{
   586  			// 1/2 members ready, should fail
   587  			[]*Member{
   588  				newTestMember(1, nil, "1", nil),
   589  				newTestMember(2, nil, "", nil),
   590  			},
   591  			false,
   592  		},
   593  		{
   594  			// 1/3 members ready, should fail
   595  			[]*Member{
   596  				newTestMember(1, nil, "1", nil),
   597  				newTestMember(2, nil, "", nil),
   598  				newTestMember(3, nil, "", nil),
   599  			},
   600  			false,
   601  		},
   602  		{
   603  			// 1/1 members ready, should succeed (special case of 1-member cluster for recovery)
   604  			[]*Member{
   605  				newTestMember(1, nil, "1", nil),
   606  			},
   607  			true,
   608  		},
   609  		{
   610  			// 2/3 members ready, should fail
   611  			[]*Member{
   612  				newTestMember(1, nil, "1", nil),
   613  				newTestMember(2, nil, "2", nil),
   614  				newTestMember(3, nil, "", nil),
   615  			},
   616  			false,
   617  		},
   618  		{
   619  			// 3/3 members ready, should be fine to add one member and retain quorum
   620  			[]*Member{
   621  				newTestMember(1, nil, "1", nil),
   622  				newTestMember(2, nil, "2", nil),
   623  				newTestMember(3, nil, "3", nil),
   624  			},
   625  			true,
   626  		},
   627  		{
   628  			// 3/4 members ready, should be fine to add one member and retain quorum
   629  			[]*Member{
   630  				newTestMember(1, nil, "1", nil),
   631  				newTestMember(2, nil, "2", nil),
   632  				newTestMember(3, nil, "3", nil),
   633  				newTestMember(4, nil, "", nil),
   634  			},
   635  			true,
   636  		},
   637  		{
   638  			// empty cluster, it is impossible but should fail
   639  			[]*Member{},
   640  			false,
   641  		},
   642  	}
   643  	for i, tt := range tests {
   644  		c := newTestCluster(tt.members)
   645  		if got := c.IsReadyToAddNewMember(); got != tt.want {
   646  			t.Errorf("%d: isReadyToAddNewMember returned %t, want %t", i, got, tt.want)
   647  		}
   648  	}
   649  }
   650  
   651  func TestIsReadyToRemoveMember(t *testing.T) {
   652  	tests := []struct {
   653  		members  []*Member
   654  		removeID uint64
   655  		want     bool
   656  	}{
   657  		{
   658  			// 1/1 members ready, should fail
   659  			[]*Member{
   660  				newTestMember(1, nil, "1", nil),
   661  			},
   662  			1,
   663  			false,
   664  		},
   665  		{
   666  			// 0/3 members ready, should fail
   667  			[]*Member{
   668  				newTestMember(1, nil, "", nil),
   669  				newTestMember(2, nil, "", nil),
   670  				newTestMember(3, nil, "", nil),
   671  			},
   672  			1,
   673  			false,
   674  		},
   675  		{
   676  			// 1/2 members ready, should be fine to remove unstarted member
   677  			// (isReadyToRemoveMember() logic should return success, but operation itself would fail)
   678  			[]*Member{
   679  				newTestMember(1, nil, "1", nil),
   680  				newTestMember(2, nil, "", nil),
   681  			},
   682  			2,
   683  			true,
   684  		},
   685  		{
   686  			// 2/3 members ready, should fail
   687  			[]*Member{
   688  				newTestMember(1, nil, "1", nil),
   689  				newTestMember(2, nil, "2", nil),
   690  				newTestMember(3, nil, "", nil),
   691  			},
   692  			2,
   693  			false,
   694  		},
   695  		{
   696  			// 3/3 members ready, should be fine to remove one member and retain quorum
   697  			[]*Member{
   698  				newTestMember(1, nil, "1", nil),
   699  				newTestMember(2, nil, "2", nil),
   700  				newTestMember(3, nil, "3", nil),
   701  			},
   702  			3,
   703  			true,
   704  		},
   705  		{
   706  			// 3/4 members ready, should be fine to remove one member
   707  			[]*Member{
   708  				newTestMember(1, nil, "1", nil),
   709  				newTestMember(2, nil, "2", nil),
   710  				newTestMember(3, nil, "3", nil),
   711  				newTestMember(4, nil, "", nil),
   712  			},
   713  			3,
   714  			true,
   715  		},
   716  		{
   717  			// 3/4 members ready, should be fine to remove unstarted member
   718  			[]*Member{
   719  				newTestMember(1, nil, "1", nil),
   720  				newTestMember(2, nil, "2", nil),
   721  				newTestMember(3, nil, "3", nil),
   722  				newTestMember(4, nil, "", nil),
   723  			},
   724  			4,
   725  			true,
   726  		},
   727  	}
   728  	for i, tt := range tests {
   729  		c := newTestCluster(tt.members)
   730  		if got := c.IsReadyToRemoveMember(tt.removeID); got != tt.want {
   731  			t.Errorf("%d: isReadyToAddNewMember returned %t, want %t", i, got, tt.want)
   732  		}
   733  	}
   734  }