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  }