etcd/raft/quorum/datadriven_test.go
Tobias Schottdorf e262542d6d quorum: fix vet failure
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.
2019-06-20 23:40:08 +02:00

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()
})
})
}