cluster_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657
  1. // Copyright 2015 CoreOS, Inc.
  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. package etcdserver
  15. import (
  16. "encoding/json"
  17. "fmt"
  18. "path"
  19. "reflect"
  20. "testing"
  21. "github.com/coreos/etcd/pkg/testutil"
  22. "github.com/coreos/etcd/pkg/types"
  23. "github.com/coreos/etcd/raft/raftpb"
  24. "github.com/coreos/etcd/store"
  25. )
  26. func TestClusterFromString(t *testing.T) {
  27. tests := []struct {
  28. f string
  29. mems []*Member
  30. }{
  31. {
  32. "mem1=http://10.0.0.1:2379,mem1=http://128.193.4.20:2379,mem2=http://10.0.0.2:2379,default=http://127.0.0.1:2379",
  33. []*Member{
  34. newTestMember(3141198903430435750, []string{"http://10.0.0.2:2379"}, "mem2", nil),
  35. newTestMember(4322322643958477905, []string{"http://10.0.0.1:2379", "http://128.193.4.20:2379"}, "mem1", nil),
  36. newTestMember(12762790032478827328, []string{"http://127.0.0.1:2379"}, "default", nil),
  37. },
  38. },
  39. }
  40. for i, tt := range tests {
  41. c, err := NewClusterFromString("abc", tt.f)
  42. if err != nil {
  43. t.Fatalf("#%d: unexpected new error: %v", i, err)
  44. }
  45. if c.token != "abc" {
  46. t.Errorf("#%d: token = %v, want abc", i, c.token)
  47. }
  48. if !reflect.DeepEqual(c.Members(), tt.mems) {
  49. t.Errorf("#%d: members = %+v, want %+v", i, c.Members(), tt.mems)
  50. }
  51. }
  52. }
  53. func TestClusterFromStringBad(t *testing.T) {
  54. tests := []string{
  55. // invalid URL
  56. "%^",
  57. // no URL defined for member
  58. "mem1=,mem2=http://128.193.4.20:2379,mem3=http://10.0.0.2:2379",
  59. "mem1,mem2=http://128.193.4.20:2379,mem3=http://10.0.0.2:2379",
  60. // bad URL for member
  61. "default=http://localhost/",
  62. // TODO(philips): anyone know of a 64 bit sha1 hash collision
  63. // "06b2f82fd81b2c20=http://128.193.4.20:2379,02c60cb75083ceef=http://128.193.4.20:2379",
  64. // the same url for two members
  65. "mem1=http://128.193.4.20:2379,mem2=http://128.193.4.20:2379",
  66. }
  67. for i, tt := range tests {
  68. if _, err := NewClusterFromString("abc", tt); err == nil {
  69. t.Errorf("#%d: unexpected successful new, want err", i)
  70. }
  71. }
  72. }
  73. func TestClusterFromStore(t *testing.T) {
  74. tests := []struct {
  75. mems []*Member
  76. }{
  77. {
  78. []*Member{newTestMember(1, nil, "", nil)},
  79. },
  80. {
  81. nil,
  82. },
  83. {
  84. []*Member{
  85. newTestMember(1, nil, "", nil),
  86. newTestMember(2, nil, "", nil),
  87. },
  88. },
  89. }
  90. for i, tt := range tests {
  91. hc := newTestCluster(nil)
  92. hc.SetStore(store.New())
  93. for _, m := range tt.mems {
  94. hc.AddMember(m)
  95. }
  96. c := NewClusterFromStore("abc", hc.store)
  97. if c.token != "abc" {
  98. t.Errorf("#%d: token = %v, want %v", i, c.token, "abc")
  99. }
  100. if !reflect.DeepEqual(c.Members(), tt.mems) {
  101. t.Errorf("#%d: members = %v, want %v", i, c.Members(), tt.mems)
  102. }
  103. }
  104. }
  105. func TestClusterMember(t *testing.T) {
  106. membs := []*Member{
  107. newTestMember(1, nil, "node1", nil),
  108. newTestMember(2, nil, "node2", nil),
  109. }
  110. tests := []struct {
  111. id types.ID
  112. match bool
  113. }{
  114. {1, true},
  115. {2, true},
  116. {3, false},
  117. }
  118. for i, tt := range tests {
  119. c := newTestCluster(membs)
  120. m := c.Member(tt.id)
  121. if g := m != nil; g != tt.match {
  122. t.Errorf("#%d: find member = %v, want %v", i, g, tt.match)
  123. }
  124. if m != nil && m.ID != tt.id {
  125. t.Errorf("#%d: id = %x, want %x", i, m.ID, tt.id)
  126. }
  127. }
  128. }
  129. func TestClusterMemberByName(t *testing.T) {
  130. membs := []*Member{
  131. newTestMember(1, nil, "node1", nil),
  132. newTestMember(2, nil, "node2", nil),
  133. }
  134. tests := []struct {
  135. name string
  136. match bool
  137. }{
  138. {"node1", true},
  139. {"node2", true},
  140. {"node3", false},
  141. }
  142. for i, tt := range tests {
  143. c := newTestCluster(membs)
  144. m := c.MemberByName(tt.name)
  145. if g := m != nil; g != tt.match {
  146. t.Errorf("#%d: find member = %v, want %v", i, g, tt.match)
  147. }
  148. if m != nil && m.Name != tt.name {
  149. t.Errorf("#%d: name = %v, want %v", i, m.Name, tt.name)
  150. }
  151. }
  152. }
  153. func TestClusterMemberIDs(t *testing.T) {
  154. c := newTestCluster([]*Member{
  155. newTestMember(1, nil, "", nil),
  156. newTestMember(4, nil, "", nil),
  157. newTestMember(100, nil, "", nil),
  158. })
  159. w := []types.ID{1, 4, 100}
  160. g := c.MemberIDs()
  161. if !reflect.DeepEqual(w, g) {
  162. t.Errorf("IDs = %+v, want %+v", g, w)
  163. }
  164. }
  165. func TestClusterPeerURLs(t *testing.T) {
  166. tests := []struct {
  167. mems []*Member
  168. wurls []string
  169. }{
  170. // single peer with a single address
  171. {
  172. mems: []*Member{
  173. newTestMember(1, []string{"http://192.0.2.1"}, "", nil),
  174. },
  175. wurls: []string{"http://192.0.2.1"},
  176. },
  177. // single peer with a single address with a port
  178. {
  179. mems: []*Member{
  180. newTestMember(1, []string{"http://192.0.2.1:8001"}, "", nil),
  181. },
  182. wurls: []string{"http://192.0.2.1:8001"},
  183. },
  184. // several members explicitly unsorted
  185. {
  186. mems: []*Member{
  187. newTestMember(2, []string{"http://192.0.2.3", "http://192.0.2.4"}, "", nil),
  188. newTestMember(3, []string{"http://192.0.2.5", "http://192.0.2.6"}, "", nil),
  189. newTestMember(1, []string{"http://192.0.2.1", "http://192.0.2.2"}, "", nil),
  190. },
  191. 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"},
  192. },
  193. // no members
  194. {
  195. mems: []*Member{},
  196. wurls: []string{},
  197. },
  198. // peer with no peer urls
  199. {
  200. mems: []*Member{
  201. newTestMember(3, []string{}, "", nil),
  202. },
  203. wurls: []string{},
  204. },
  205. }
  206. for i, tt := range tests {
  207. c := newTestCluster(tt.mems)
  208. urls := c.PeerURLs()
  209. if !reflect.DeepEqual(urls, tt.wurls) {
  210. t.Errorf("#%d: PeerURLs = %v, want %v", i, urls, tt.wurls)
  211. }
  212. }
  213. }
  214. func TestClusterClientURLs(t *testing.T) {
  215. tests := []struct {
  216. mems []*Member
  217. wurls []string
  218. }{
  219. // single peer with a single address
  220. {
  221. mems: []*Member{
  222. newTestMember(1, nil, "", []string{"http://192.0.2.1"}),
  223. },
  224. wurls: []string{"http://192.0.2.1"},
  225. },
  226. // single peer with a single address with a port
  227. {
  228. mems: []*Member{
  229. newTestMember(1, nil, "", []string{"http://192.0.2.1:8001"}),
  230. },
  231. wurls: []string{"http://192.0.2.1:8001"},
  232. },
  233. // several members explicitly unsorted
  234. {
  235. mems: []*Member{
  236. newTestMember(2, nil, "", []string{"http://192.0.2.3", "http://192.0.2.4"}),
  237. newTestMember(3, nil, "", []string{"http://192.0.2.5", "http://192.0.2.6"}),
  238. newTestMember(1, nil, "", []string{"http://192.0.2.1", "http://192.0.2.2"}),
  239. },
  240. 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"},
  241. },
  242. // no members
  243. {
  244. mems: []*Member{},
  245. wurls: []string{},
  246. },
  247. // peer with no client urls
  248. {
  249. mems: []*Member{
  250. newTestMember(3, nil, "", []string{}),
  251. },
  252. wurls: []string{},
  253. },
  254. }
  255. for i, tt := range tests {
  256. c := newTestCluster(tt.mems)
  257. urls := c.ClientURLs()
  258. if !reflect.DeepEqual(urls, tt.wurls) {
  259. t.Errorf("#%d: ClientURLs = %v, want %v", i, urls, tt.wurls)
  260. }
  261. }
  262. }
  263. func TestClusterValidateAndAssignIDsBad(t *testing.T) {
  264. tests := []struct {
  265. clmembs []*Member
  266. membs []*Member
  267. }{
  268. {
  269. // unmatched length
  270. []*Member{
  271. newTestMember(1, []string{"http://127.0.0.1:2379"}, "", nil),
  272. },
  273. []*Member{},
  274. },
  275. {
  276. // unmatched peer urls
  277. []*Member{
  278. newTestMember(1, []string{"http://127.0.0.1:2379"}, "", nil),
  279. },
  280. []*Member{
  281. newTestMember(1, []string{"http://127.0.0.1:4001"}, "", nil),
  282. },
  283. },
  284. {
  285. // unmatched peer urls
  286. []*Member{
  287. newTestMember(1, []string{"http://127.0.0.1:2379"}, "", nil),
  288. newTestMember(2, []string{"http://127.0.0.2:2379"}, "", nil),
  289. },
  290. []*Member{
  291. newTestMember(1, []string{"http://127.0.0.1:2379"}, "", nil),
  292. newTestMember(2, []string{"http://127.0.0.2:4001"}, "", nil),
  293. },
  294. },
  295. }
  296. for i, tt := range tests {
  297. ecl := newTestCluster(tt.clmembs)
  298. lcl := newTestCluster(tt.membs)
  299. if err := ValidateClusterAndAssignIDs(lcl, ecl); err == nil {
  300. t.Errorf("#%d: unexpected update success", i)
  301. }
  302. }
  303. }
  304. func TestClusterValidateAndAssignIDs(t *testing.T) {
  305. tests := []struct {
  306. clmembs []*Member
  307. membs []*Member
  308. wids []types.ID
  309. }{
  310. {
  311. []*Member{
  312. newTestMember(1, []string{"http://127.0.0.1:2379"}, "", nil),
  313. newTestMember(2, []string{"http://127.0.0.2:2379"}, "", nil),
  314. },
  315. []*Member{
  316. newTestMember(3, []string{"http://127.0.0.1:2379"}, "", nil),
  317. newTestMember(4, []string{"http://127.0.0.2:2379"}, "", nil),
  318. },
  319. []types.ID{3, 4},
  320. },
  321. }
  322. for i, tt := range tests {
  323. lcl := newTestCluster(tt.clmembs)
  324. ecl := newTestCluster(tt.membs)
  325. if err := ValidateClusterAndAssignIDs(lcl, ecl); err != nil {
  326. t.Errorf("#%d: unexpect update error: %v", i, err)
  327. }
  328. if !reflect.DeepEqual(lcl.MemberIDs(), tt.wids) {
  329. t.Errorf("#%d: ids = %v, want %v", i, lcl.MemberIDs(), tt.wids)
  330. }
  331. }
  332. }
  333. func TestClusterValidateConfigurationChange(t *testing.T) {
  334. cl := newCluster("")
  335. cl.SetStore(store.New())
  336. for i := 1; i <= 4; i++ {
  337. attr := RaftAttributes{PeerURLs: []string{fmt.Sprintf("http://127.0.0.1:%d", i)}}
  338. cl.AddMember(&Member{ID: types.ID(i), RaftAttributes: attr})
  339. }
  340. cl.RemoveMember(4)
  341. attr := RaftAttributes{PeerURLs: []string{fmt.Sprintf("http://127.0.0.1:%d", 1)}}
  342. ctx, err := json.Marshal(&Member{ID: types.ID(5), RaftAttributes: attr})
  343. if err != nil {
  344. t.Fatal(err)
  345. }
  346. attr = RaftAttributes{PeerURLs: []string{fmt.Sprintf("http://127.0.0.1:%d", 5)}}
  347. ctx5, err := json.Marshal(&Member{ID: types.ID(5), RaftAttributes: attr})
  348. if err != nil {
  349. t.Fatal(err)
  350. }
  351. attr = RaftAttributes{PeerURLs: []string{fmt.Sprintf("http://127.0.0.1:%d", 3)}}
  352. ctx2to3, err := json.Marshal(&Member{ID: types.ID(2), RaftAttributes: attr})
  353. if err != nil {
  354. t.Fatal(err)
  355. }
  356. attr = RaftAttributes{PeerURLs: []string{fmt.Sprintf("http://127.0.0.1:%d", 5)}}
  357. ctx2to5, err := json.Marshal(&Member{ID: types.ID(2), RaftAttributes: attr})
  358. if err != nil {
  359. t.Fatal(err)
  360. }
  361. tests := []struct {
  362. cc raftpb.ConfChange
  363. werr error
  364. }{
  365. {
  366. raftpb.ConfChange{
  367. Type: raftpb.ConfChangeRemoveNode,
  368. NodeID: 3,
  369. },
  370. nil,
  371. },
  372. {
  373. raftpb.ConfChange{
  374. Type: raftpb.ConfChangeAddNode,
  375. NodeID: 4,
  376. },
  377. ErrIDRemoved,
  378. },
  379. {
  380. raftpb.ConfChange{
  381. Type: raftpb.ConfChangeRemoveNode,
  382. NodeID: 4,
  383. },
  384. ErrIDRemoved,
  385. },
  386. {
  387. raftpb.ConfChange{
  388. Type: raftpb.ConfChangeAddNode,
  389. NodeID: 1,
  390. },
  391. ErrIDExists,
  392. },
  393. {
  394. raftpb.ConfChange{
  395. Type: raftpb.ConfChangeAddNode,
  396. NodeID: 5,
  397. Context: ctx,
  398. },
  399. ErrPeerURLexists,
  400. },
  401. {
  402. raftpb.ConfChange{
  403. Type: raftpb.ConfChangeRemoveNode,
  404. NodeID: 5,
  405. },
  406. ErrIDNotFound,
  407. },
  408. {
  409. raftpb.ConfChange{
  410. Type: raftpb.ConfChangeAddNode,
  411. NodeID: 5,
  412. Context: ctx5,
  413. },
  414. nil,
  415. },
  416. {
  417. raftpb.ConfChange{
  418. Type: raftpb.ConfChangeUpdateNode,
  419. NodeID: 5,
  420. Context: ctx,
  421. },
  422. ErrIDNotFound,
  423. },
  424. // try to change the peer url of 2 to the peer url of 3
  425. {
  426. raftpb.ConfChange{
  427. Type: raftpb.ConfChangeUpdateNode,
  428. NodeID: 2,
  429. Context: ctx2to3,
  430. },
  431. ErrPeerURLexists,
  432. },
  433. {
  434. raftpb.ConfChange{
  435. Type: raftpb.ConfChangeUpdateNode,
  436. NodeID: 2,
  437. Context: ctx2to5,
  438. },
  439. nil,
  440. },
  441. }
  442. for i, tt := range tests {
  443. err := cl.ValidateConfigurationChange(tt.cc)
  444. if err != tt.werr {
  445. t.Errorf("#%d: validateConfigurationChange error = %v, want %v", i, err, tt.werr)
  446. }
  447. }
  448. }
  449. func TestClusterGenID(t *testing.T) {
  450. cs := newTestCluster([]*Member{
  451. newTestMember(1, nil, "", nil),
  452. newTestMember(2, nil, "", nil),
  453. })
  454. cs.genID()
  455. if cs.ID() == 0 {
  456. t.Fatalf("cluster.ID = %v, want not 0", cs.ID())
  457. }
  458. previd := cs.ID()
  459. cs.SetStore(&storeRecorder{})
  460. cs.AddMember(newTestMember(3, nil, "", nil))
  461. cs.genID()
  462. if cs.ID() == previd {
  463. t.Fatalf("cluster.ID = %v, want not %v", cs.ID(), previd)
  464. }
  465. }
  466. func TestNodeToMemberBad(t *testing.T) {
  467. tests := []*store.NodeExtern{
  468. {Key: "/1234", Nodes: []*store.NodeExtern{
  469. {Key: "/1234/strange"},
  470. }},
  471. {Key: "/1234", Nodes: []*store.NodeExtern{
  472. {Key: "/1234/raftAttributes", Value: stringp("garbage")},
  473. }},
  474. {Key: "/1234", Nodes: []*store.NodeExtern{
  475. {Key: "/1234/attributes", Value: stringp(`{"name":"node1","clientURLs":null}`)},
  476. }},
  477. {Key: "/1234", Nodes: []*store.NodeExtern{
  478. {Key: "/1234/raftAttributes", Value: stringp(`{"peerURLs":null}`)},
  479. {Key: "/1234/strange"},
  480. }},
  481. {Key: "/1234", Nodes: []*store.NodeExtern{
  482. {Key: "/1234/raftAttributes", Value: stringp(`{"peerURLs":null}`)},
  483. {Key: "/1234/attributes", Value: stringp("garbage")},
  484. }},
  485. {Key: "/1234", Nodes: []*store.NodeExtern{
  486. {Key: "/1234/raftAttributes", Value: stringp(`{"peerURLs":null}`)},
  487. {Key: "/1234/attributes", Value: stringp(`{"name":"node1","clientURLs":null}`)},
  488. {Key: "/1234/strange"},
  489. }},
  490. }
  491. for i, tt := range tests {
  492. if _, err := nodeToMember(tt); err == nil {
  493. t.Errorf("#%d: unexpected nil error", i)
  494. }
  495. }
  496. }
  497. func TestClusterAddMember(t *testing.T) {
  498. st := &storeRecorder{}
  499. c := newTestCluster(nil)
  500. c.SetStore(st)
  501. c.AddMember(newTestMember(1, nil, "node1", nil))
  502. wactions := []testutil.Action{
  503. {
  504. Name: "Create",
  505. Params: []interface{}{
  506. path.Join(storeMembersPrefix, "1", "raftAttributes"),
  507. false,
  508. `{"peerURLs":null}`,
  509. false,
  510. store.Permanent,
  511. },
  512. },
  513. }
  514. if g := st.Action(); !reflect.DeepEqual(g, wactions) {
  515. t.Errorf("actions = %v, want %v", g, wactions)
  516. }
  517. }
  518. func TestClusterMembers(t *testing.T) {
  519. cls := &Cluster{
  520. members: map[types.ID]*Member{
  521. 1: &Member{ID: 1},
  522. 20: &Member{ID: 20},
  523. 100: &Member{ID: 100},
  524. 5: &Member{ID: 5},
  525. 50: &Member{ID: 50},
  526. },
  527. }
  528. w := []*Member{
  529. &Member{ID: 1},
  530. &Member{ID: 5},
  531. &Member{ID: 20},
  532. &Member{ID: 50},
  533. &Member{ID: 100},
  534. }
  535. if g := cls.Members(); !reflect.DeepEqual(g, w) {
  536. t.Fatalf("Members()=%#v, want %#v", g, w)
  537. }
  538. }
  539. func TestClusterString(t *testing.T) {
  540. cls := &Cluster{
  541. members: map[types.ID]*Member{
  542. 1: newTestMember(
  543. 1,
  544. []string{"http://1.1.1.1:1111", "http://0.0.0.0:0000"},
  545. "abc",
  546. nil,
  547. ),
  548. 2: newTestMember(
  549. 2,
  550. []string{"http://2.2.2.2:2222"},
  551. "def",
  552. nil,
  553. ),
  554. 3: newTestMember(
  555. 3,
  556. []string{"http://3.3.3.3:1234", "http://127.0.0.1:7001"},
  557. "ghi",
  558. nil,
  559. ),
  560. // no PeerURLs = not included
  561. 4: newTestMember(
  562. 4,
  563. []string{},
  564. "four",
  565. nil,
  566. ),
  567. 5: newTestMember(
  568. 5,
  569. nil,
  570. "five",
  571. nil,
  572. ),
  573. },
  574. }
  575. w := "abc=http://0.0.0.0:0000,abc=http://1.1.1.1:1111,def=http://2.2.2.2:2222,ghi=http://127.0.0.1:7001,ghi=http://3.3.3.3:1234"
  576. if g := cls.String(); g != w {
  577. t.Fatalf("Cluster.String():\ngot %#v\nwant %#v", g, w)
  578. }
  579. }
  580. func TestClusterRemoveMember(t *testing.T) {
  581. st := &storeRecorder{}
  582. c := newTestCluster(nil)
  583. c.SetStore(st)
  584. c.RemoveMember(1)
  585. wactions := []testutil.Action{
  586. {Name: "Delete", Params: []interface{}{memberStoreKey(1), true, true}},
  587. {Name: "Create", Params: []interface{}{removedMemberStoreKey(1), false, "", false, store.Permanent}},
  588. }
  589. if !reflect.DeepEqual(st.Action(), wactions) {
  590. t.Errorf("actions = %v, want %v", st.Action(), wactions)
  591. }
  592. }
  593. func TestNodeToMember(t *testing.T) {
  594. n := &store.NodeExtern{Key: "/1234", Nodes: []*store.NodeExtern{
  595. {Key: "/1234/attributes", Value: stringp(`{"name":"node1","clientURLs":null}`)},
  596. {Key: "/1234/raftAttributes", Value: stringp(`{"peerURLs":null}`)},
  597. }}
  598. wm := &Member{ID: 0x1234, RaftAttributes: RaftAttributes{}, Attributes: Attributes{Name: "node1"}}
  599. m, err := nodeToMember(n)
  600. if err != nil {
  601. t.Fatalf("unexpected nodeToMember error: %v", err)
  602. }
  603. if !reflect.DeepEqual(m, wm) {
  604. t.Errorf("member = %+v, want %+v", m, wm)
  605. }
  606. }
  607. func newTestCluster(membs []*Member) *Cluster {
  608. c := &Cluster{members: make(map[types.ID]*Member), removed: make(map[types.ID]bool)}
  609. for _, m := range membs {
  610. c.members[m.ID] = m
  611. }
  612. return c
  613. }
  614. func stringp(s string) *string { return &s }