mirror of
https://github.com/etcd-io/etcd.git
synced 2024-09-27 06:25:44 +00:00

This slipped in during a rename and I didn't see it in CI because of CI flakiness and a general intransparency about which failures are important.
251 lines
8.3 KiB
Go
251 lines
8.3 KiB
Go
// Copyright 2019 The etcd Authors
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package quorum
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/cockroachdb/datadriven"
|
|
)
|
|
|
|
// TestDataDriven parses and executes the test cases in ./testdata/*. An entry
|
|
// in such a file specifies the command, which is either of "committed" to check
|
|
// CommittedIndex or "vote" to verify a VoteResult. The underlying configuration
|
|
// and inputs are specified via the arguments 'cfg' and 'cfgj' (for the majority
|
|
// config and, optionally, majority config joint to the first one) and 'idx'
|
|
// (for CommittedIndex) and 'votes' (for VoteResult).
|
|
//
|
|
// Internally, the harness runs some additional checks on each test case for
|
|
// which it is known that the result shouldn't change. For example,
|
|
// interchanging the majority configurations of a joint quorum must not
|
|
// influence the result; if it does, this is noted in the test's output.
|
|
func TestDataDriven(t *testing.T) {
|
|
datadriven.Walk(t, "testdata", func(t *testing.T, path string) {
|
|
datadriven.RunTest(t, path, func(d *datadriven.TestData) string {
|
|
// Two majority configs. The first one is always used (though it may
|
|
// be empty) and the second one is used iff joint is true.
|
|
var joint bool
|
|
var ids, idsj []uint64
|
|
// The committed indexes for the nodes in the config in the order in
|
|
// which they appear in (ids,idsj), without repetition. An underscore
|
|
// denotes an omission (i.e. no information for this voter); this is
|
|
// different from 0. For example,
|
|
//
|
|
// cfg=(1,2) cfgj=(2,3,4) idxs=(_,5,_,7) initializes the idx for voter 2
|
|
// to 5 and that for voter 4 to 7 (and no others).
|
|
//
|
|
// cfgj=zero is specified to instruct the test harness to treat cfgj
|
|
// as zero instead of not specified (i.e. it will trigger a joint
|
|
// quorum test instead of a majority quorum test for cfg only).
|
|
var idxs []Index
|
|
// Votes. These are initialized similar to idxs except the only values
|
|
// used are 1 (voted against) and 2 (voted for). This looks awkward,
|
|
// but is convenient because it allows sharing code between the two.
|
|
var votes []Index
|
|
|
|
// Parse the args.
|
|
for _, arg := range d.CmdArgs {
|
|
for i := range arg.Vals {
|
|
switch arg.Key {
|
|
case "cfg":
|
|
var n uint64
|
|
arg.Scan(t, i, &n)
|
|
ids = append(ids, n)
|
|
case "cfgj":
|
|
joint = true
|
|
if arg.Vals[i] == "zero" {
|
|
if len(arg.Vals) != 1 {
|
|
t.Fatalf("cannot mix 'zero' into configuration")
|
|
}
|
|
} else {
|
|
var n uint64
|
|
arg.Scan(t, i, &n)
|
|
idsj = append(idsj, n)
|
|
}
|
|
case "idx":
|
|
var n uint64
|
|
// Register placeholders as zeroes.
|
|
if arg.Vals[i] != "_" {
|
|
arg.Scan(t, i, &n)
|
|
if n == 0 {
|
|
// This is a restriction caused by the above
|
|
// special-casing for _.
|
|
t.Fatalf("cannot use 0 as idx")
|
|
}
|
|
}
|
|
idxs = append(idxs, Index(n))
|
|
case "votes":
|
|
var s string
|
|
arg.Scan(t, i, &s)
|
|
switch s {
|
|
case "y":
|
|
votes = append(votes, 2)
|
|
case "n":
|
|
votes = append(votes, 1)
|
|
case "_":
|
|
votes = append(votes, 0)
|
|
default:
|
|
t.Fatalf("unknown vote: %s", s)
|
|
}
|
|
default:
|
|
t.Fatalf("unknown arg %s", arg.Key)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build the two majority configs.
|
|
c := MajorityConfig{}
|
|
for _, id := range ids {
|
|
c[id] = struct{}{}
|
|
}
|
|
cj := MajorityConfig{}
|
|
for _, id := range idsj {
|
|
cj[id] = struct{}{}
|
|
}
|
|
|
|
// Helper that returns an AckedIndexer which has the specified indexes
|
|
// mapped to the right IDs.
|
|
makeLookuper := func(idxs []Index, ids, idsj []uint64) mapAckIndexer {
|
|
l := mapAckIndexer{}
|
|
var p int // next to consume from idxs
|
|
for _, id := range append(append([]uint64(nil), ids...), idsj...) {
|
|
if _, ok := l[id]; ok {
|
|
continue
|
|
}
|
|
if p < len(idxs) {
|
|
// NB: this creates zero entries for placeholders that we remove later.
|
|
// The upshot of doing it that way is to avoid having to specify place-
|
|
// holders multiple times when omitting voters present in both halves of
|
|
// a joint config.
|
|
l[id] = idxs[p]
|
|
p++
|
|
}
|
|
}
|
|
|
|
for id := range l {
|
|
// Zero entries are created by _ placeholders; we don't want
|
|
// them in the lookuper because "no entry" is different from
|
|
// "zero entry". Note that we prevent tests from specifying
|
|
// zero commit indexes, so that there's no confusion between
|
|
// the two concepts.
|
|
if l[id] == 0 {
|
|
delete(l, id)
|
|
}
|
|
}
|
|
return l
|
|
}
|
|
|
|
{
|
|
input := idxs
|
|
if d.Cmd == "vote" {
|
|
input = votes
|
|
}
|
|
if voters := JointConfig([2]MajorityConfig{c, cj}).IDs(); len(voters) != len(input) {
|
|
return fmt.Sprintf("error: mismatched input (explicit or _) for voters %v: %v",
|
|
voters, input)
|
|
}
|
|
}
|
|
|
|
var buf strings.Builder
|
|
switch d.Cmd {
|
|
case "committed":
|
|
l := makeLookuper(idxs, ids, idsj)
|
|
|
|
// Branch based on whether this is a majority or joint quorum
|
|
// test case.
|
|
if !joint {
|
|
idx := c.CommittedIndex(l)
|
|
fmt.Fprintf(&buf, c.Describe(l))
|
|
// These alternative computations should return the same
|
|
// result. If not, print to the output.
|
|
if aIdx := alternativeMajorityCommittedIndex(c, l); aIdx != idx {
|
|
fmt.Fprintf(&buf, "%s <-- via alternative computation\n", aIdx)
|
|
}
|
|
// Joining a majority with the empty majority should give same result.
|
|
if aIdx := JointConfig([2]MajorityConfig{c, {}}).CommittedIndex(l); aIdx != idx {
|
|
fmt.Fprintf(&buf, "%s <-- via zero-joint quorum\n", aIdx)
|
|
}
|
|
// Joining a majority with itself should give same result.
|
|
if aIdx := JointConfig([2]MajorityConfig{c, c}).CommittedIndex(l); aIdx != idx {
|
|
fmt.Fprintf(&buf, "%s <-- via self-joint quorum\n", aIdx)
|
|
}
|
|
overlay := func(c MajorityConfig, l AckedIndexer, id uint64, idx Index) AckedIndexer {
|
|
ll := mapAckIndexer{}
|
|
for iid := range c {
|
|
if iid == id {
|
|
ll[iid] = idx
|
|
} else if idx, ok := l.AckedIndex(iid); ok {
|
|
ll[iid] = idx
|
|
}
|
|
}
|
|
return ll
|
|
}
|
|
for id := range c {
|
|
iidx, _ := l.AckedIndex(id)
|
|
if idx > iidx && iidx > 0 {
|
|
// If the committed index was definitely above the currently
|
|
// inspected idx, the result shouldn't change if we lower it
|
|
// further.
|
|
lo := overlay(c, l, id, iidx-1)
|
|
if aIdx := c.CommittedIndex(lo); aIdx != idx {
|
|
fmt.Fprintf(&buf, "%s <-- overlaying %d->%d", aIdx, id, iidx)
|
|
}
|
|
lo = overlay(c, l, id, 0)
|
|
if aIdx := c.CommittedIndex(lo); aIdx != idx {
|
|
fmt.Fprintf(&buf, "%s <-- overlaying %d->0", aIdx, id)
|
|
}
|
|
}
|
|
}
|
|
fmt.Fprintf(&buf, "%s\n", idx)
|
|
} else {
|
|
cc := JointConfig([2]MajorityConfig{c, cj})
|
|
fmt.Fprintf(&buf, cc.Describe(l))
|
|
idx := cc.CommittedIndex(l)
|
|
// Interchanging the majorities shouldn't make a difference. If it does, print.
|
|
if aIdx := JointConfig([2]MajorityConfig{c, cj}).CommittedIndex(l); aIdx != idx {
|
|
fmt.Fprintf(&buf, "%s <-- via symmetry\n", aIdx)
|
|
}
|
|
fmt.Fprintf(&buf, "%s\n", idx)
|
|
}
|
|
case "vote":
|
|
ll := makeLookuper(votes, ids, idsj)
|
|
l := map[uint64]bool{}
|
|
for id, v := range ll {
|
|
l[id] = v != 1 // NB: 1 == false, 2 == true
|
|
}
|
|
|
|
if !joint {
|
|
// Test a majority quorum.
|
|
r := c.VoteResult(l)
|
|
fmt.Fprintf(&buf, "%v\n", r)
|
|
} else {
|
|
// Run a joint quorum test case.
|
|
r := JointConfig([2]MajorityConfig{c, cj}).VoteResult(l)
|
|
// Interchanging the majorities shouldn't make a difference. If it does, print.
|
|
if ar := JointConfig([2]MajorityConfig{cj, c}).VoteResult(l); ar != r {
|
|
fmt.Fprintf(&buf, "%v <-- via symmetry\n", ar)
|
|
}
|
|
fmt.Fprintf(&buf, "%v\n", r)
|
|
}
|
|
default:
|
|
t.Fatalf("unknown command: %s", d.Cmd)
|
|
}
|
|
return buf.String()
|
|
})
|
|
})
|
|
}
|