github.com/juju/juju@v0.0.0-20240327075706-a90865de2538/worker/peergrouper/desired_test.go (about) 1 // Copyright 2014 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package peergrouper 5 6 import ( 7 "fmt" 8 "net" 9 "sort" 10 "strconv" 11 "strings" 12 13 "github.com/juju/replicaset/v3" 14 "github.com/juju/testing" 15 jc "github.com/juju/testing/checkers" 16 gc "gopkg.in/check.v1" 17 18 "github.com/juju/juju/core/network" 19 "github.com/juju/juju/state" 20 ) 21 22 type desiredPeerGroupSuite struct { 23 testing.IsolationSuite 24 } 25 26 var _ = gc.Suite(&desiredPeerGroupSuite{}) 27 28 const ( 29 mongoPort = 1234 30 apiPort = 5678 31 controllerAPIPort = 9876 32 ) 33 34 type desiredPeerGroupTest struct { 35 about string 36 machines []*controllerTracker 37 statuses []replicaset.MemberStatus 38 members []replicaset.Member 39 40 expectChanged bool 41 expectStepDown bool 42 expectMembers []replicaset.Member 43 expectVoting []bool 44 expectErr string 45 } 46 47 // TestMember mirrors replicaset.Member but simplifies the structure 48 // so that test assertions are easier to understand. 49 // 50 // See http://docs.mongodb.org/manual/reference/replica-configuration/ 51 // for more details 52 type TestMember struct { 53 // Id is a unique id for a member in a set. 54 Id int 55 56 // Address holds the network address of the member, 57 // in the form hostname:port. 58 Address string 59 60 // Priority determines eligibility of a member to become primary. 61 // This value is optional; it defaults to 1. 62 Priority float64 63 64 // Tags store additional information about a replica member, often used for 65 // customizing read preferences and write concern. 66 Tags map[string]string 67 68 // Votes controls the number of votes a server has in a replica set election. 69 // This value is optional; it defaults to 1. 70 Votes int 71 } 72 73 func memberToTestMember(m replicaset.Member) TestMember { 74 75 priority := 1.0 76 if m.Priority != nil { 77 priority = *m.Priority 78 } 79 votes := 1 80 if m.Votes != nil { 81 votes = *m.Votes 82 } 83 return TestMember{ 84 Id: m.Id, 85 Address: m.Address, 86 Priority: priority, 87 Tags: m.Tags, 88 Votes: votes, 89 } 90 } 91 92 func membersToTestMembers(m []replicaset.Member) []TestMember { 93 if m == nil { 94 return nil 95 } 96 result := make([]TestMember, len(m)) 97 for i, member := range m { 98 result[i] = memberToTestMember(member) 99 } 100 return result 101 } 102 103 func desiredPeerGroupTests(ipVersion TestIPVersion) []desiredPeerGroupTest { 104 return []desiredPeerGroupTest{ 105 { 106 about: "one controller, one more proposed member", 107 machines: mkMachines("10v 11v", ipVersion), 108 statuses: mkStatuses("0p", ipVersion), 109 members: mkMembers("0v", ipVersion), 110 expectMembers: mkMembers("0v 1", ipVersion), 111 expectVoting: []bool{true, false}, 112 expectChanged: true, 113 }, { 114 about: "one controller, two more proposed members", 115 machines: mkMachines("10v 11v 12v", ipVersion), 116 statuses: mkStatuses("0p", ipVersion), 117 members: mkMembers("0v", ipVersion), 118 expectMembers: mkMembers("0v 1 2", ipVersion), 119 expectVoting: []bool{true, false, false}, 120 expectChanged: true, 121 }, { 122 about: "single controller, no change", 123 machines: mkMachines("11v", ipVersion), 124 members: mkMembers("1v", ipVersion), 125 statuses: mkStatuses("1p", ipVersion), 126 expectVoting: []bool{true}, 127 expectMembers: mkMembers("1v", ipVersion), 128 expectChanged: false, 129 }, { 130 about: "extra member with nil Vote", 131 machines: mkMachines("11v", ipVersion), 132 members: mkMembers("1vT 2v", ipVersion), 133 statuses: mkStatuses("1p 2s", ipVersion), 134 expectVoting: []bool{true}, 135 expectErr: "non juju voting member.* found in peer group", 136 }, { 137 about: "extra member with >1 votes", 138 machines: mkMachines("11v", ipVersion), 139 members: append(mkMembers("1vT", ipVersion), replicaset.Member{ 140 Id: 2, 141 Votes: newInt(2), 142 Address: net.JoinHostPort( 143 fmt.Sprintf(ipVersion.formatHost, 12), 144 fmt.Sprint(mongoPort), 145 ), 146 }), 147 statuses: mkStatuses("1p 2s", ipVersion), 148 expectVoting: []bool{true}, 149 expectErr: "non juju voting member.* found in peer group", 150 }, { 151 about: "one controller has become ready to vote (no change)", 152 machines: mkMachines("11v 12v", ipVersion), 153 members: mkMembers("1v 2", ipVersion), 154 statuses: mkStatuses("1p 2s", ipVersion), 155 expectVoting: []bool{true, false}, 156 expectMembers: mkMembers("1v 2", ipVersion), 157 expectChanged: false, 158 }, { 159 about: "two machines have become ready to vote (-> added)", 160 machines: mkMachines("11v 12v 13v", ipVersion), 161 members: mkMembers("1v 2 3", ipVersion), 162 statuses: mkStatuses("1p 2s 3s", ipVersion), 163 expectVoting: []bool{true, true, true}, 164 expectMembers: mkMembers("1v 2v 3v", ipVersion), 165 expectChanged: true, 166 }, { 167 about: "one controller has become ready to vote but one is not healthy", 168 machines: mkMachines("11v 12v", ipVersion), 169 members: mkMembers("1v 2", ipVersion), 170 statuses: mkStatuses("1p 2sH", ipVersion), 171 expectVoting: []bool{true, false}, 172 expectMembers: mkMembers("1v 2", ipVersion), 173 expectChanged: false, 174 }, { 175 about: "two machines have become ready to vote but one is not healthy (-> no change)", 176 machines: mkMachines("11v 12v 13v", ipVersion), 177 members: mkMembers("1v 2 3", ipVersion), 178 statuses: mkStatuses("1p 2s 3sH", ipVersion), 179 expectVoting: []bool{true, false, false}, 180 expectMembers: mkMembers("1v 2 3", ipVersion), 181 expectChanged: false, 182 }, { 183 about: "three machines have become ready to vote (-> 2 added)", 184 machines: mkMachines("11v 12v 13v 14v", ipVersion), 185 members: mkMembers("1v 2 3 4", ipVersion), 186 statuses: mkStatuses("1p 2s 3s 4s", ipVersion), 187 expectVoting: []bool{true, true, true, false}, 188 expectMembers: mkMembers("1v 2v 3v 4", ipVersion), 189 expectChanged: true, 190 }, { 191 about: "one controller ready to lose vote with no others -> no change", 192 machines: mkMachines("11", ipVersion), 193 members: mkMembers("1v", ipVersion), 194 statuses: mkStatuses("1p", ipVersion), 195 expectVoting: []bool{true}, 196 expectMembers: mkMembers("1v", ipVersion), 197 expectChanged: false, 198 }, { 199 about: "one controller ready to lose vote -> votes removed from secondaries", 200 machines: mkMachines("11v 12v 13", ipVersion), 201 members: mkMembers("1v 2v 3v", ipVersion), 202 statuses: mkStatuses("1s 2p 3s", ipVersion), 203 expectVoting: []bool{false, true, false}, 204 expectMembers: mkMembers("1 2v 3", ipVersion), 205 expectChanged: true, 206 }, { 207 about: "two machines ready to lose vote -> votes removed", 208 machines: mkMachines("11 12v 13", ipVersion), 209 members: mkMembers("1v 2v 3v", ipVersion), 210 statuses: mkStatuses("1s 2p 3s", ipVersion), 211 expectVoting: []bool{false, true, false}, 212 expectMembers: mkMembers("1 2v 3", ipVersion), 213 expectChanged: true, 214 }, { 215 about: "machines removed as controller -> removed from members", 216 machines: mkMachines("11v", ipVersion), 217 members: mkMembers("1v 2 3", ipVersion), 218 statuses: mkStatuses("1p 2s 3s", ipVersion), 219 expectVoting: []bool{true}, 220 expectMembers: mkMembers("1v", ipVersion), 221 expectChanged: true, 222 }, { 223 about: "controller dead -> removed from members", 224 machines: append(mkMachines("11v 12v", ipVersion), &controllerTracker{ 225 id: "13", 226 addresses: []network.SpaceAddress{{ 227 MachineAddress: network.MachineAddress{ 228 Value: ipVersion.extraHost, 229 Type: ipVersion.addressType, 230 Scope: network.ScopeCloudLocal, 231 }, 232 }}, 233 host: mkController(state.Dead), 234 }), 235 members: mkMembers("1v 2v 3v", ipVersion), 236 statuses: mkStatuses("1p 2s 3s", ipVersion), 237 expectVoting: []bool{true, false}, 238 expectMembers: mkMembers("1v 2s", ipVersion), 239 expectChanged: true, 240 }, { 241 about: "controller removed as controller -> removed from member", 242 machines: mkMachines("11v 12", ipVersion), 243 members: mkMembers("1v 2 3", ipVersion), 244 statuses: mkStatuses("1p 2s 3s", ipVersion), 245 expectVoting: []bool{true, false}, 246 expectMembers: mkMembers("1v 2", ipVersion), 247 expectChanged: true, 248 }, { 249 about: "a candidate can take the vote of a non-candidate when they're ready", 250 machines: mkMachines("11v 12v 13 14v", ipVersion), 251 members: mkMembers("1v 2v 3v 4", ipVersion), 252 statuses: mkStatuses("1p 2s 3s 4s", ipVersion), 253 expectVoting: []bool{true, true, false, true}, 254 expectMembers: mkMembers("1v 2v 3 4v", ipVersion), 255 expectChanged: true, 256 }, { 257 about: "several candidates can take non-candidates' votes", 258 machines: mkMachines("11v 12v 13 14 15 16v 17v 18v", ipVersion), 259 members: mkMembers("1v 2v 3v 4v 5v 6 7 8", ipVersion), 260 statuses: mkStatuses("1p 2s 3s 4s 5s 6s 7s 8s", ipVersion), 261 expectVoting: []bool{true, true, false, false, false, true, true, true}, 262 expectMembers: mkMembers("1v 2v 3 4 5 6v 7v 8v", ipVersion), 263 expectChanged: true, 264 }, { 265 about: "a changed controller address should propagate to the members", 266 machines: append(mkMachines("11v 12v", ipVersion), &controllerTracker{ 267 id: "13", 268 wantsVote: true, 269 addresses: []network.SpaceAddress{{ 270 MachineAddress: network.MachineAddress{ 271 Value: ipVersion.extraHost, 272 Type: ipVersion.addressType, 273 Scope: network.ScopeCloudLocal, 274 }, 275 }}, 276 host: mkController(state.Alive), 277 }), 278 statuses: mkStatuses("1s 2p 3s", ipVersion), 279 members: mkMembers("1v 2v 3v", ipVersion), 280 expectVoting: []bool{true, true, true}, 281 expectMembers: append(mkMembers("1v 2v", ipVersion), replicaset.Member{ 282 Id: 3, 283 Address: net.JoinHostPort(ipVersion.extraHost, fmt.Sprint(mongoPort)), 284 Tags: memberTag("13"), 285 }), 286 expectChanged: true, 287 }, { 288 about: "a controller's address is ignored if it changes to empty", 289 machines: append(mkMachines("11v 12v", ipVersion), &controllerTracker{ 290 id: "13", 291 wantsVote: true, 292 host: mkController(state.Alive), 293 }), 294 statuses: mkStatuses("1s 2p 3s", ipVersion), 295 members: mkMembers("1v 2v 3v", ipVersion), 296 expectVoting: []bool{true, true, true}, 297 expectMembers: mkMembers("1v 2v 3v", ipVersion), 298 expectChanged: false, 299 }, { 300 about: "two voting members removes vote from secondary (first member)", 301 machines: mkMachines("11v 12v", ipVersion), 302 members: mkMembers("1v 2v", ipVersion), 303 statuses: mkStatuses("1s 2p", ipVersion), 304 expectVoting: []bool{false, true}, 305 expectMembers: mkMembers("1 2v", ipVersion), 306 expectChanged: true, 307 }, { 308 about: "two voting members removes vote from secondary (second member)", 309 machines: mkMachines("11v 12v", ipVersion), 310 members: mkMembers("1v 2v", ipVersion), 311 statuses: mkStatuses("1p 2s", ipVersion), 312 expectVoting: []bool{true, false}, 313 expectMembers: mkMembers("1v 2", ipVersion), 314 expectChanged: true, 315 }, { 316 about: "three voting members one ready to loose voting -> no consensus", 317 machines: mkMachines("11v 12v 13", ipVersion), 318 members: mkMembers("1v 2v 3v", ipVersion), 319 statuses: mkStatuses("1p 2s 3s", ipVersion), 320 expectVoting: []bool{true, false, false}, 321 expectMembers: mkMembers("1v 2 3", ipVersion), 322 expectChanged: true, 323 }, { 324 about: "three voting members remove one, to only one voting member left", 325 machines: mkMachines("11v 12", ipVersion), 326 members: mkMembers("1v 2v 3", ipVersion), 327 statuses: mkStatuses("1p 2s 3s", ipVersion), 328 expectVoting: []bool{true, false}, 329 expectMembers: mkMembers("1v 2", ipVersion), 330 expectChanged: true, 331 }, { 332 about: "three voting members remove all, keep primary", 333 machines: mkMachines("11 12 13", ipVersion), 334 members: mkMembers("1v 2v 3v", ipVersion), 335 statuses: mkStatuses("1s 2s 3p", ipVersion), 336 expectVoting: []bool{false, false, true}, 337 expectMembers: mkMembers("1 2 3v", ipVersion), 338 expectChanged: true, 339 }, { 340 about: "add controller, non-voting still add it to the replica set", 341 machines: mkMachines("11v 12v 13v 14", ipVersion), 342 members: mkMembers("1v 2v 3v", ipVersion), 343 statuses: mkStatuses("1s 2s 3p", ipVersion), 344 expectVoting: []bool{true, true, true, false}, 345 expectMembers: mkMembers("1v 2v 3v 4", ipVersion), 346 expectChanged: true, 347 }, { 348 about: "remove primary controller", 349 machines: mkMachines("11 12v 13v", ipVersion), 350 members: mkMembers("1v 2v 3v", ipVersion), 351 statuses: mkStatuses("1p 2s 3s", ipVersion), 352 expectVoting: []bool{false, false, true}, 353 expectMembers: mkMembers("1 2 3v", ipVersion), 354 expectStepDown: true, 355 expectChanged: true, 356 }, 357 } 358 } 359 360 func (s *desiredPeerGroupSuite) TestDesiredPeerGroupIPv4(c *gc.C) { 361 s.doTestDesiredPeerGroup(c, testIPv4) 362 } 363 364 func (s *desiredPeerGroupSuite) TestDesiredPeerGroupIPv6(c *gc.C) { 365 s.doTestDesiredPeerGroup(c, testIPv6) 366 } 367 368 func (s *desiredPeerGroupSuite) doTestDesiredPeerGroup(c *gc.C, ipVersion TestIPVersion) { 369 for ti, test := range desiredPeerGroupTests(ipVersion) { 370 c.Logf("\ntest %d: %s", ti, test.about) 371 trackerMap := make(map[string]*controllerTracker) 372 for _, m := range test.machines { 373 c.Assert(trackerMap[m.Id()], gc.IsNil) 374 trackerMap[m.Id()] = m 375 } 376 377 info, err := newPeerGroupInfo(trackerMap, test.statuses, test.members, mongoPort, network.SpaceInfo{}) 378 c.Assert(err, jc.ErrorIsNil) 379 380 desired, err := desiredPeerGroup(info) 381 if test.expectErr != "" { 382 c.Assert(err, gc.ErrorMatches, test.expectErr) 383 c.Assert(desired.members, gc.IsNil) 384 c.Assert(desired.isChanged, jc.IsFalse) 385 continue 386 } 387 c.Assert(err, jc.ErrorIsNil) 388 c.Assert(info, gc.NotNil) 389 390 members := make([]replicaset.Member, 0, len(desired.members)) 391 for _, m := range desired.members { 392 members = append(members, *m) 393 } 394 395 sort.Sort(membersById(members)) 396 c.Assert(desired.isChanged, gc.Equals, test.expectChanged) 397 c.Assert(desired.stepDownPrimary, gc.Equals, test.expectStepDown) 398 c.Assert(membersToTestMembers(members), jc.DeepEquals, membersToTestMembers(test.expectMembers)) 399 for i, m := range test.machines { 400 if m.host.Life() == state.Dead { 401 continue 402 } 403 var vote, votePresent bool 404 for _, member := range desired.members { 405 controllerId, ok := member.Tags[jujuNodeKey] 406 c.Assert(ok, jc.IsTrue) 407 if controllerId == m.Id() { 408 votePresent = true 409 vote = isVotingMember(member) 410 break 411 } 412 } 413 c.Check(votePresent, jc.IsTrue) 414 c.Check(vote, gc.Equals, test.expectVoting[i], gc.Commentf("controller %s", m.Id())) 415 } 416 417 // Assure ourselves that the total number of desired votes is odd in 418 // all circumstances. 419 c.Assert(countVotes(members)%2, gc.Equals, 1) 420 421 // Make sure that when the members are set as required, that there 422 // is no further change if desiredPeerGroup is called again. 423 info, err = newPeerGroupInfo(trackerMap, test.statuses, members, mongoPort, network.SpaceInfo{}) 424 c.Assert(err, jc.ErrorIsNil) 425 c.Assert(info, gc.NotNil) 426 427 desired, err = desiredPeerGroup(info) 428 c.Assert(err, jc.ErrorIsNil) 429 c.Assert(desired.isChanged, jc.IsFalse) 430 c.Assert(desired.stepDownPrimary, jc.IsFalse) 431 countPrimaries := 0 432 c.Assert(err, gc.IsNil) 433 for i, m := range test.machines { 434 var vote, votePresent bool 435 for _, member := range desired.members { 436 controllerId, ok := member.Tags[jujuNodeKey] 437 c.Assert(ok, jc.IsTrue) 438 if controllerId == m.Id() { 439 votePresent = true 440 vote = isVotingMember(member) 441 break 442 } 443 } 444 if m.host.Life() == state.Dead { 445 c.Assert(votePresent, jc.IsFalse) 446 continue 447 } 448 c.Check(votePresent, jc.IsTrue) 449 c.Check(vote, gc.Equals, test.expectVoting[i], gc.Commentf("controller %s", m.Id())) 450 if isPrimaryMember(info, m.Id()) { 451 countPrimaries += 1 452 } 453 } 454 c.Assert(countPrimaries, gc.Equals, 1) 455 c.Assert(err, jc.ErrorIsNil) 456 } 457 } 458 459 func (s *desiredPeerGroupSuite) TestIsPrimary(c *gc.C) { 460 machines := mkMachines("11v 12v 13v", testIPv4) 461 trackerMap := make(map[string]*controllerTracker) 462 for _, m := range machines { 463 c.Assert(trackerMap[m.Id()], gc.IsNil) 464 trackerMap[m.Id()] = m 465 } 466 members := mkMembers("1v 2v 3v", testIPv4) 467 statuses := mkStatuses("1p 2s 3s", testIPv4) 468 info, err := newPeerGroupInfo(trackerMap, statuses, members, mongoPort, network.SpaceInfo{}) 469 c.Assert(err, jc.ErrorIsNil) 470 isPrimary, err := info.isPrimary("11") 471 c.Assert(err, jc.ErrorIsNil) 472 c.Assert(isPrimary, jc.IsTrue) 473 isPrimary, err = info.isPrimary("12") 474 c.Assert(err, jc.ErrorIsNil) 475 c.Assert(isPrimary, jc.IsFalse) 476 } 477 478 func (s *desiredPeerGroupSuite) TestNewPeerGroupInfoErrWhenNoMembers(c *gc.C) { 479 _, err := newPeerGroupInfo(nil, nil, nil, 666, network.SpaceInfo{}) 480 c.Check(err, gc.ErrorMatches, "current member set is empty") 481 } 482 483 func (s *desiredPeerGroupSuite) TestCheckExtraMembersReturnsErrorWhenVoterFound(c *gc.C) { 484 v := 1 485 peerChanges := peerGroupChanges{ 486 info: &peerGroupInfo{extra: []replicaset.Member{{Votes: &v}}}, 487 } 488 err := peerChanges.checkExtraMembers() 489 c.Check(err, gc.ErrorMatches, "non juju voting member .+ found in peer group") 490 } 491 492 func (s *desiredPeerGroupSuite) TestCheckExtraMembersReturnsTrueWhenCheckMade(c *gc.C) { 493 v := 0 494 peerChanges := peerGroupChanges{ 495 info: &peerGroupInfo{extra: []replicaset.Member{{Votes: &v}}}, 496 } 497 err := peerChanges.checkExtraMembers() 498 c.Check(peerChanges.desired.isChanged, jc.IsTrue) 499 c.Check(err, jc.ErrorIsNil) 500 } 501 502 func (s *desiredPeerGroupSuite) TestCheckToRemoveMembersReturnsTrueWhenCheckMade(c *gc.C) { 503 peerChanges := peerGroupChanges{ 504 info: &peerGroupInfo{toRemove: []replicaset.Member{{}}}, 505 } 506 err := peerChanges.checkExtraMembers() 507 c.Check(peerChanges.desired.isChanged, jc.IsTrue) 508 c.Check(err, jc.ErrorIsNil) 509 } 510 511 func (s *desiredPeerGroupSuite) TestCheckExtraMembersReturnsFalseWhenEmpty(c *gc.C) { 512 peerChanges := peerGroupChanges{ 513 info: &peerGroupInfo{}, 514 } 515 err := peerChanges.checkExtraMembers() 516 c.Check(peerChanges.desired.isChanged, jc.IsFalse) 517 c.Check(err, jc.ErrorIsNil) 518 } 519 520 func countVotes(members []replicaset.Member) int { 521 tot := 0 522 for _, m := range members { 523 v := 1 524 if m.Votes != nil { 525 v = *m.Votes 526 } 527 tot += v 528 } 529 return tot 530 } 531 532 func newInt(i int) *int { 533 return &i 534 } 535 536 func newFloat64(f float64) *float64 { 537 return &f 538 } 539 540 func mkController(life state.Life) *fakeController { 541 ctrl := &fakeController{} 542 ctrl.val.Set(controllerDoc{life: life}) 543 return ctrl 544 } 545 546 // mkMachines returns a slice of *machineTracker based on 547 // the given description. 548 // Each controller in the description is white-space separated 549 // and holds the decimal controller id followed by an optional 550 // "v" if the controller wants a vote. 551 func mkMachines(description string, ipVersion TestIPVersion) []*controllerTracker { 552 descrs := parseDescr(description) 553 ms := make([]*controllerTracker, len(descrs)) 554 for i, d := range descrs { 555 ms[i] = &controllerTracker{ 556 id: fmt.Sprint(d.id), 557 addresses: []network.SpaceAddress{{ 558 MachineAddress: network.MachineAddress{ 559 Value: fmt.Sprintf(ipVersion.formatHost, d.id), 560 Type: ipVersion.addressType, 561 Scope: network.ScopeCloudLocal, 562 }, 563 }}, 564 host: mkController(state.Alive), 565 wantsVote: strings.Contains(d.flags, "v"), 566 } 567 } 568 return ms 569 } 570 571 func memberTag(id string) map[string]string { 572 return map[string]string{jujuNodeKey: id} 573 } 574 575 // mkMembers returns a slice of replicaset.Member based on the given 576 // description. 577 // Each member in the description is white-space separated and holds the decimal 578 // replica-set id optionally followed by the characters: 579 // - 'v' if the member is voting. 580 // - 'T' if the member has no associated controller tags. 581 // 582 // Unless the T flag is specified, the controller tag 583 // will be the replica-set id + 10. 584 func mkMembers(description string, ipVersion TestIPVersion) []replicaset.Member { 585 descrs := parseDescr(description) 586 ms := make([]replicaset.Member, len(descrs)) 587 for i, d := range descrs { 588 machineId := d.id + 10 589 m := replicaset.Member{ 590 Id: d.id, 591 Address: net.JoinHostPort( 592 fmt.Sprintf(ipVersion.formatHost, machineId), 593 fmt.Sprint(mongoPort), 594 ), 595 Tags: memberTag(fmt.Sprint(machineId)), 596 } 597 if !strings.Contains(d.flags, "v") { 598 m.Priority = newFloat64(0) 599 m.Votes = newInt(0) 600 } 601 if strings.Contains(d.flags, "T") { 602 m.Tags = nil 603 } 604 ms[i] = m 605 } 606 return ms 607 } 608 609 var stateFlags = map[rune]replicaset.MemberState{ 610 'p': replicaset.PrimaryState, 611 's': replicaset.SecondaryState, 612 } 613 614 // mkStatuses returns a slice of replicaset.MemberStatus based on the given 615 // description. 616 // Each member in the description is white-space separated and holds the 617 // decimal replica-set id optionally followed by the characters: 618 // - 'H' if the instance is not healthy. 619 // - 'p' if the instance is in PrimaryState 620 // - 's' if the instance is in SecondaryState 621 func mkStatuses(description string, ipVersion TestIPVersion) []replicaset.MemberStatus { 622 descrs := parseDescr(description) 623 ss := make([]replicaset.MemberStatus, len(descrs)) 624 for i, d := range descrs { 625 machineId := d.id + 10 626 s := replicaset.MemberStatus{ 627 Id: d.id, 628 Address: net.JoinHostPort( 629 fmt.Sprintf(ipVersion.formatHost, machineId), 630 fmt.Sprint(mongoPort), 631 ), 632 Healthy: !strings.Contains(d.flags, "H"), 633 State: replicaset.UnknownState, 634 } 635 for _, r := range d.flags { 636 if state, ok := stateFlags[r]; ok { 637 s.State = state 638 } 639 } 640 ss[i] = s 641 } 642 return ss 643 } 644 645 type descr struct { 646 id int 647 flags string 648 } 649 650 func isNotDigit(r rune) bool { 651 return r < '0' || r > '9' 652 } 653 654 var parseDescrTests = []struct { 655 descr string 656 expect []descr 657 }{{ 658 descr: "", 659 expect: []descr{}, 660 }, { 661 descr: "0", 662 expect: []descr{{id: 0}}, 663 }, { 664 descr: "1foo", 665 expect: []descr{{id: 1, flags: "foo"}}, 666 }, { 667 descr: "10c 5 6443arble ", 668 expect: []descr{{ 669 id: 10, 670 flags: "c", 671 }, { 672 id: 5, 673 }, { 674 id: 6443, 675 flags: "arble", 676 }}, 677 }} 678 679 func (*desiredPeerGroupSuite) TestParseDescr(c *gc.C) { 680 for i, test := range parseDescrTests { 681 c.Logf("test %d. %q", i, test.descr) 682 c.Assert(parseDescr(test.descr), jc.DeepEquals, test.expect) 683 } 684 } 685 686 // parseDescr parses white-space separated fields of the form 687 // <id><flags> into descr structures. 688 func parseDescr(s string) []descr { 689 fields := strings.Fields(s) 690 descrs := make([]descr, len(fields)) 691 for i, field := range fields { 692 d := &descrs[i] 693 i := strings.IndexFunc(field, isNotDigit) 694 if i == -1 { 695 i = len(field) 696 } 697 id, err := strconv.Atoi(field[0:i]) 698 if err != nil { 699 panic(fmt.Errorf("bad field %q", field)) 700 } 701 d.id = id 702 d.flags = field[i:] 703 } 704 return descrs 705 } 706 707 func assertMembers(c *gc.C, obtained interface{}, expected []replicaset.Member) { 708 c.Assert(obtained, gc.FitsTypeOf, []replicaset.Member{}) 709 // Avoid mutating the obtained slice: because it's usually retrieved 710 // directly from the memberWatcher voyeur.Value, 711 // mutation can cause races. 712 obtainedMembers := deepCopy(obtained).([]replicaset.Member) 713 sort.Sort(membersById(obtainedMembers)) 714 sort.Sort(membersById(expected)) 715 c.Assert(membersToTestMembers(obtainedMembers), jc.DeepEquals, membersToTestMembers(expected)) 716 } 717 718 type membersById []replicaset.Member 719 720 func (l membersById) Len() int { return len(l) } 721 func (l membersById) Swap(i, j int) { l[i], l[j] = l[j], l[i] } 722 func (l membersById) Less(i, j int) bool { return l[i].Id < l[j].Id } 723 724 // AssertAPIHostPorts asserts of two sets of network.SpaceHostPort slices are the same. 725 func AssertAPIHostPorts(c *gc.C, got, want []network.SpaceHostPorts) { 726 c.Assert(got, gc.HasLen, len(want)) 727 sort.Sort(hostPortSliceByHostPort(got)) 728 sort.Sort(hostPortSliceByHostPort(want)) 729 c.Assert(got, gc.DeepEquals, want) 730 } 731 732 type hostPortSliceByHostPort []network.SpaceHostPorts 733 734 func (h hostPortSliceByHostPort) Len() int { return len(h) } 735 func (h hostPortSliceByHostPort) Swap(i, j int) { h[i], h[j] = h[j], h[i] } 736 func (h hostPortSliceByHostPort) Less(i, j int) bool { 737 a, b := h[i], h[j] 738 if len(a) != len(b) { 739 return len(a) < len(b) 740 } 741 for i := range a { 742 av, bv := a[i], b[i] 743 if av.Value != bv.Value { 744 return av.Value < bv.Value 745 } 746 if av.Port() != bv.Port() { 747 return av.Port() < bv.Port() 748 } 749 } 750 return false 751 } 752 753 type sortAsIntsSuite struct { 754 testing.IsolationSuite 755 } 756 757 var _ = gc.Suite(&sortAsIntsSuite{}) 758 759 func checkIntSorted(c *gc.C, vals, expected []string) { 760 // we sort in place, so leave 'vals' alone and copy to another slice 761 copied := append([]string(nil), vals...) 762 sortAsInts(copied) 763 c.Check(copied, gc.DeepEquals, expected) 764 } 765 766 func (*sortAsIntsSuite) TestAllInts(c *gc.C) { 767 checkIntSorted(c, []string{"1", "10", "2", "20"}, []string{"1", "2", "10", "20"}) 768 } 769 770 func (*sortAsIntsSuite) TestStrings(c *gc.C) { 771 checkIntSorted(c, []string{"a", "c", "b", "X"}, []string{"X", "a", "b", "c"}) 772 } 773 774 func (*sortAsIntsSuite) TestMixed(c *gc.C) { 775 checkIntSorted(c, []string{"1", "20", "10", "2", "2d", "c", "b", "X"}, 776 []string{"1", "2", "10", "20", "2d", "X", "b", "c"}) 777 }