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