Files
tendermint/blockchain/v2/scheduler_test.go
Sean Braithwaite 55278211ed Blockchain v2 Scheduler (#4043)
* Add processor prototype

* Change processor API

    + expose a simple `handle` function which mutates internal state

* schedule event handling

* rename schedule -> scheduler

* fill in handle function

* processor tests

* fix gofmt and ohter golangci issues

* scopelint var on range scope

* add check for short block received

* small test reorg

* ci fix changes

* go.mod revert

* some cleanup and review comments

* scheduler fixes and unit tests, also small processor changes.

changed scPeerPruned to include a list of pruned peers

touchPeer to check peer state and remove the blocks from blockStates if the peer removal causes the max peer height to be lower.

remove the block at sc.initHeight

changed peersInactiveSince, peersSlowerThan, getPeersAtHeight check peer state

prunablePeers to return a sorted list of peers

lastRate in markReceived() attempted to divide by 0, temp fix.

fixed allBlocksProcessed conditions

maxHeight() and minHeight() to return sc.initHeight if no ready peers present

make selectPeer() deterministic.

added handleBlockProcessError()

added termination cond. (sc.allBlocksProcessed()) to handleTryPrunePeer() and others.

changed pcBlockVerificationFailure to include peer of H+2 block along with the one for H+1

changed the processor to call purgePeer on block verification failure.

fixed processor tests

added scheduler tests.

* typo and ci fixes

* remove height from scBlockRequest, golangci fixes

* limit on blockState map, updated tests

* remove unused

* separate test for maxHeight(), used for sched. validation

* use Math.Min

* fix golangci

* Document the semantics of blockStates in the scheduler

* better docs

* distinguish between unknown and invalid blockstate

* Standardize peer filtering methods

* feedback

* s/getPeersAtHeight/getPeersAtHeightOrAbove

* small notes

* Update blockchain/v2/scheduler.go

Co-Authored-By: Anton Kaliaev <anton.kalyaev@gmail.com>

* Update comments based on feedback

* Add enum offset

* panic on nil block in processor

* remove unused max height calculation

* format shorter line
2019-12-02 11:01:24 +01:00

2294 lines
66 KiB
Go

package v2
import (
"fmt"
"math"
"sort"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/tendermint/tendermint/p2p"
"github.com/tendermint/tendermint/types"
)
type scTestParams struct {
peers map[string]*scPeer
initHeight int64
height int64
allB []int64
pending map[int64]p2p.ID
pendingTime map[int64]time.Time
received map[int64]p2p.ID
peerTimeout time.Duration
minRecvRate int64
targetPending int
}
func verifyScheduler(sc *scheduler) {
missing := 0
if sc.maxHeight() >= sc.height {
missing = int(math.Min(float64(sc.targetPending), float64(sc.maxHeight()-sc.height+1)))
}
if len(sc.blockStates) != missing {
panic(fmt.Sprintf("scheduler block length %d different than target %d", len(sc.blockStates), missing))
}
}
func newTestScheduler(params scTestParams) *scheduler {
peers := make(map[p2p.ID]*scPeer)
sc := newScheduler(params.initHeight)
if params.height != 0 {
sc.height = params.height
}
for id, peer := range params.peers {
peer.peerID = p2p.ID(id)
peers[p2p.ID(id)] = peer
}
for _, h := range params.allB {
sc.blockStates[h] = blockStateNew
}
for h, pid := range params.pending {
sc.blockStates[h] = blockStatePending
sc.pendingBlocks[h] = pid
}
for h, tm := range params.pendingTime {
sc.pendingTime[h] = tm
}
for h, pid := range params.received {
sc.blockStates[h] = blockStateReceived
sc.receivedBlocks[h] = pid
}
sc.peers = peers
sc.peerTimeout = params.peerTimeout
if params.targetPending == 0 {
sc.targetPending = 10
} else {
sc.targetPending = params.targetPending
}
sc.minRecvRate = params.minRecvRate
verifyScheduler(sc)
return sc
}
func TestScInit(t *testing.T) {
var (
initHeight int64 = 5
sc = newScheduler(initHeight)
)
assert.Equal(t, blockStateProcessed, sc.getStateAtHeight(initHeight))
assert.Equal(t, blockStateUnknown, sc.getStateAtHeight(initHeight+1))
}
func TestScMaxHeights(t *testing.T) {
tests := []struct {
name string
sc scheduler
wantMax int64
}{
{
name: "no peers",
sc: scheduler{height: 11},
wantMax: 10,
},
{
name: "one ready peer",
sc: scheduler{
initHeight: 2,
height: 3,
peers: map[p2p.ID]*scPeer{"P1": {height: 6, state: peerStateReady}},
},
wantMax: 6,
},
{
name: "ready and removed peers",
sc: scheduler{
height: 1,
peers: map[p2p.ID]*scPeer{
"P1": {height: 4, state: peerStateReady},
"P2": {height: 10, state: peerStateRemoved}},
},
wantMax: 4,
},
{
name: "removed peers",
sc: scheduler{
height: 1,
peers: map[p2p.ID]*scPeer{
"P1": {height: 4, state: peerStateRemoved},
"P2": {height: 10, state: peerStateRemoved}},
},
wantMax: 0,
},
{
name: "new peers",
sc: scheduler{
height: 1,
peers: map[p2p.ID]*scPeer{
"P1": {height: -1, state: peerStateNew},
"P2": {height: -1, state: peerStateNew}},
},
wantMax: 0,
},
{
name: "mixed peers",
sc: scheduler{
height: 1,
peers: map[p2p.ID]*scPeer{
"P1": {height: -1, state: peerStateNew},
"P2": {height: 10, state: peerStateReady},
"P3": {height: 20, state: peerStateRemoved},
"P4": {height: 22, state: peerStateReady},
},
},
wantMax: 22,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
// maxHeight() should not mutate the scheduler
wantSc := tt.sc
resMax := tt.sc.maxHeight()
assert.Equal(t, tt.wantMax, resMax)
assert.Equal(t, wantSc, tt.sc)
})
}
}
func TestScAddPeer(t *testing.T) {
type args struct {
peerID p2p.ID
}
tests := []struct {
name string
fields scTestParams
args args
wantFields scTestParams
wantErr bool
}{
{
name: "add first peer",
fields: scTestParams{},
args: args{peerID: "P1"},
wantFields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateNew, height: -1}}},
},
{
name: "add second peer",
fields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateNew, height: -1}}},
args: args{peerID: "P2"},
wantFields: scTestParams{peers: map[string]*scPeer{
"P1": {state: peerStateNew, height: -1},
"P2": {state: peerStateNew, height: -1}}},
},
{
name: "attempt to add duplicate peer",
fields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateNew, height: -1}}},
args: args{peerID: "P1"},
wantFields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateNew, height: -1}}},
wantErr: true,
},
{
name: "attempt to add duplicate peer with existing peer in Ready state",
fields: scTestParams{
peers: map[string]*scPeer{"P1": {state: peerStateReady, height: 3}},
allB: []int64{1, 2, 3},
},
args: args{peerID: "P1"},
wantErr: true,
wantFields: scTestParams{
peers: map[string]*scPeer{"P1": {state: peerStateReady, height: 3}},
allB: []int64{1, 2, 3},
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
sc := newTestScheduler(tt.fields)
if err := sc.addPeer(tt.args.peerID); (err != nil) != tt.wantErr {
t.Errorf("scAddPeer() wantErr %v, error = %v", tt.wantErr, err)
}
wantSc := newTestScheduler(tt.wantFields)
assert.Equal(t, wantSc, sc, "wanted peers %v, got %v", wantSc.peers, sc.peers)
})
}
}
func TestScTouchPeer(t *testing.T) {
now := time.Now()
type args struct {
peerID p2p.ID
time time.Time
}
tests := []struct {
name string
fields scTestParams
args args
wantFields scTestParams
wantErr bool
}{
{
name: "attempt to touch non existing peer",
fields: scTestParams{
peers: map[string]*scPeer{"P1": {state: peerStateReady, height: 5}},
allB: []int64{1, 2, 3, 4, 5},
},
args: args{peerID: "P2", time: now},
wantFields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateReady, height: 5}},
allB: []int64{1, 2, 3, 4, 5},
},
wantErr: true,
},
{
name: "attempt to touch peer in state New",
fields: scTestParams{peers: map[string]*scPeer{"P1": {}}},
args: args{peerID: "P1", time: now},
wantFields: scTestParams{peers: map[string]*scPeer{"P1": {}}},
wantErr: true,
},
{
name: "attempt to touch peer in state Removed",
fields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateRemoved}, "P2": {state: peerStateReady}}},
args: args{peerID: "P1", time: now},
wantFields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateRemoved}, "P2": {state: peerStateReady}}},
wantErr: true,
},
{
name: "touch peer in state Ready",
fields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateReady, lastTouched: now}}},
args: args{peerID: "P1", time: now.Add(3 * time.Second)},
wantFields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateReady,
lastTouched: now.Add(3 * time.Second)}}},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
sc := newTestScheduler(tt.fields)
if err := sc.touchPeer(tt.args.peerID, tt.args.time); (err != nil) != tt.wantErr {
t.Errorf("touchPeer() wantErr %v, error = %v", tt.wantErr, err)
}
wantSc := newTestScheduler(tt.wantFields)
assert.Equal(t, wantSc, sc, "wanted peers %v, got %v", wantSc.peers, sc.peers)
})
}
}
func TestScPeersInactiveSince(t *testing.T) {
now := time.Now()
type args struct {
threshold time.Duration
time time.Time
}
tests := []struct {
name string
fields scTestParams
args args
wantResult []p2p.ID
}{
{
name: "no peers",
fields: scTestParams{peers: map[string]*scPeer{}},
args: args{threshold: time.Second, time: now},
wantResult: []p2p.ID{},
},
{
name: "one active peer",
fields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateReady, lastTouched: now}}},
args: args{threshold: time.Second, time: now},
wantResult: []p2p.ID{},
},
{
name: "one inactive peer",
fields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateReady, lastTouched: now}}},
args: args{threshold: time.Second, time: now.Add(time.Second + time.Millisecond)},
wantResult: []p2p.ID{"P1"},
},
{
name: "one active and one inactive peer",
fields: scTestParams{peers: map[string]*scPeer{
"P1": {state: peerStateReady, lastTouched: now},
"P2": {state: peerStateReady, lastTouched: now.Add(time.Second)}}},
args: args{threshold: time.Second, time: now.Add(time.Second + time.Millisecond)},
wantResult: []p2p.ID{"P1"},
},
{
name: "one New peer",
fields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateNew}}},
args: args{threshold: time.Second, time: now.Add(time.Millisecond)},
wantResult: []p2p.ID{},
},
{
name: "one Removed peer",
fields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateRemoved, lastTouched: now}}},
args: args{threshold: time.Second, time: now.Add(time.Millisecond)},
wantResult: []p2p.ID{},
},
{
name: "one Ready active peer and one New",
fields: scTestParams{peers: map[string]*scPeer{
"P1": {state: peerStateRemoved, lastTouched: now},
"P2": {state: peerStateReady, lastTouched: now.Add(time.Millisecond)}}},
args: args{threshold: time.Second, time: now.Add(2 * time.Millisecond)},
wantResult: []p2p.ID{},
},
{
name: "one Ready inactive peer and one New",
fields: scTestParams{peers: map[string]*scPeer{
"P1": {state: peerStateRemoved, lastTouched: now},
"P2": {state: peerStateReady, lastTouched: now.Add(time.Millisecond)}}},
args: args{threshold: time.Second, time: now.Add(time.Second + 2*time.Millisecond)},
wantResult: []p2p.ID{"P2"},
},
{
name: "combination of New, Removed and, active and non active Ready peers",
fields: scTestParams{peers: map[string]*scPeer{
"P1": {state: peerStateNew},
"P2": {state: peerStateRemoved, lastTouched: now},
"P3": {state: peerStateRemoved, lastTouched: now.Add(time.Second)},
"P4": {state: peerStateReady, lastTouched: now.Add(time.Millisecond)},
"P5": {state: peerStateReady, lastTouched: now.Add(3 * time.Millisecond)}}},
args: args{threshold: time.Second, time: now.Add(time.Second + 2*time.Millisecond)},
wantResult: []p2p.ID{"P4"},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
sc := newTestScheduler(tt.fields)
// peersInactiveSince should not mutate the scheduler
wantSc := sc
res := sc.peersInactiveSince(tt.args.threshold, tt.args.time)
sort.Sort(PeerByID(res))
assert.Equal(t, tt.wantResult, res)
assert.Equal(t, wantSc, sc)
})
}
}
func TestScPeersSlowerThan(t *testing.T) {
type args struct {
minSpeed int64
}
tests := []struct {
name string
fields scTestParams
args args
wantResult []p2p.ID
}{
{
name: "no peers",
fields: scTestParams{peers: map[string]*scPeer{}},
args: args{minSpeed: 100},
wantResult: []p2p.ID{},
},
{
name: "one Ready faster peer",
fields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateReady, lastRate: 101}}},
args: args{minSpeed: 100},
wantResult: []p2p.ID{},
},
{
name: "one Ready equal peer",
fields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateReady, lastRate: 100}}},
args: args{minSpeed: 100},
wantResult: []p2p.ID{},
},
{
name: "one Ready slow peer",
fields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateReady, lastRate: 99}}},
args: args{minSpeed: 100},
wantResult: []p2p.ID{"P1"},
},
{
name: "one Removed faster peer",
fields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateRemoved, lastRate: 101}}},
args: args{minSpeed: 100},
wantResult: []p2p.ID{},
}, {
name: "one Removed equal peer",
fields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateRemoved, lastRate: 100}}},
args: args{minSpeed: 100},
wantResult: []p2p.ID{},
},
{
name: "one Removed slow peer",
fields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateRemoved, lastRate: 99}}},
args: args{minSpeed: 100},
wantResult: []p2p.ID{},
},
{
name: "one New peer",
fields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateNew}}},
args: args{minSpeed: 100},
wantResult: []p2p.ID{},
},
{
name: "one New peer",
fields: scTestParams{peers: map[string]*scPeer{"P1": {state: peerStateNew}}},
args: args{minSpeed: 100},
wantResult: []p2p.ID{},
},
{
name: "mixed peers",
fields: scTestParams{peers: map[string]*scPeer{
"P1": {state: peerStateRemoved, lastRate: 101},
"P2": {state: peerStateReady, lastRate: 101},
"P3": {state: peerStateRemoved, lastRate: 100},
"P4": {state: peerStateReady, lastRate: 100},
"P5": {state: peerStateReady, lastRate: 99},
"P6": {state: peerStateNew},
"P7": {state: peerStateRemoved, lastRate: 99},
"P8": {state: peerStateReady, lastRate: 99},
}},
args: args{minSpeed: 100},
wantResult: []p2p.ID{"P5", "P8"},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
sc := newTestScheduler(tt.fields)
// peersSlowerThan should not mutate the scheduler
wantSc := sc
res := sc.peersSlowerThan(tt.args.minSpeed)
assert.Equal(t, tt.wantResult, res)
assert.Equal(t, wantSc, sc)
})
}
}
func TestScPrunablePeers(t *testing.T) {
now := time.Now()
type args struct {
threshold time.Duration
time time.Time
minSpeed int64
}
tests := []struct {
name string
fields scTestParams
args args
wantResult []p2p.ID
}{
{
name: "no peers",
fields: scTestParams{peers: map[string]*scPeer{}},
args: args{threshold: time.Second, time: now.Add(time.Second + time.Millisecond), minSpeed: 100},
wantResult: []p2p.ID{},
},
{
name: "mixed peers",
fields: scTestParams{peers: map[string]*scPeer{
// X - removed, active, fast
"P1": {state: peerStateRemoved, lastTouched: now.Add(time.Second), lastRate: 101},
// X - ready, active, fast
"P2": {state: peerStateReady, lastTouched: now.Add(time.Second), lastRate: 101},
// X - removed, active, equal
"P3": {state: peerStateRemoved, lastTouched: now.Add(time.Second), lastRate: 100},
// V - ready, inactive, equal
"P4": {state: peerStateReady, lastTouched: now, lastRate: 100},
// V - ready, inactive, slow
"P5": {state: peerStateReady, lastTouched: now, lastRate: 99},
// V - ready, active, slow
"P6": {state: peerStateReady, lastTouched: now.Add(time.Second), lastRate: 90},
}},
args: args{threshold: time.Second, time: now.Add(time.Second + time.Millisecond), minSpeed: 100},
wantResult: []p2p.ID{"P4", "P5", "P6"},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
sc := newTestScheduler(tt.fields)
// peersSlowerThan should not mutate the scheduler
wantSc := sc
res := sc.prunablePeers(tt.args.threshold, tt.args.minSpeed, tt.args.time)
assert.Equal(t, tt.wantResult, res)
assert.Equal(t, wantSc, sc)
})
}
}
func TestScRemovePeer(t *testing.T) {
type args struct {
peerID p2p.ID
}
tests := []struct {
name string
fields scTestParams
args args
wantFields scTestParams
wantErr bool
}{
{
name: "remove non existing peer",
fields: scTestParams{peers: map[string]*scPeer{"P1": {height: -1}}},
args: args{peerID: "P2"},
wantFields: scTestParams{peers: map[string]*scPeer{"P1": {height: -1}}},
wantErr: true,
},
{
name: "remove single New peer",
fields: scTestParams{peers: map[string]*scPeer{"P1": {height: -1}}},
args: args{peerID: "P1"},
wantFields: scTestParams{peers: map[string]*scPeer{"P1": {height: -1, state: peerStateRemoved}}},
},
{
name: "remove one of two New peers",
fields: scTestParams{peers: map[string]*scPeer{"P1": {height: -1}, "P2": {height: -1}}},
args: args{peerID: "P1"},
wantFields: scTestParams{peers: map[string]*scPeer{"P1": {height: -1, state: peerStateRemoved}, "P2": {height: -1}}},
},
{
name: "remove one Ready peer, all peers removed",
fields: scTestParams{
peers: map[string]*scPeer{
"P1": {height: 10, state: peerStateRemoved},
"P2": {height: 5, state: peerStateReady}},
allB: []int64{1, 2, 3, 4, 5},
},
args: args{peerID: "P2"},
wantFields: scTestParams{peers: map[string]*scPeer{
"P1": {height: 10, state: peerStateRemoved},
"P2": {height: 5, state: peerStateRemoved}},
},
},
{
name: "attempt to remove already removed peer",
fields: scTestParams{
height: 8,
peers: map[string]*scPeer{
"P1": {height: 10, state: peerStateRemoved},
"P2": {height: 11, state: peerStateReady}},
allB: []int64{8, 9, 10, 11},
},
args: args{peerID: "P1"},
wantFields: scTestParams{
height: 8,
peers: map[string]*scPeer{
"P1": {height: 10, state: peerStateRemoved},
"P2": {height: 11, state: peerStateReady}},
allB: []int64{8, 9, 10, 11}},
wantErr: true,
},
{
name: "remove Ready peer with blocks requested",
fields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 3, state: peerStateReady}},
allB: []int64{1, 2, 3},
pending: map[int64]p2p.ID{1: "P1"},
},
args: args{peerID: "P1"},
wantFields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 3, state: peerStateRemoved}},
allB: []int64{},
pending: map[int64]p2p.ID{},
},
},
{
name: "remove Ready peer with blocks received",
fields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 3, state: peerStateReady}},
allB: []int64{1, 2, 3},
received: map[int64]p2p.ID{1: "P1"},
},
args: args{peerID: "P1"},
wantFields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 3, state: peerStateRemoved}},
allB: []int64{},
received: map[int64]p2p.ID{},
},
},
{
name: "remove Ready peer with blocks received and requested (not yet received)",
fields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
allB: []int64{1, 2, 3, 4},
pending: map[int64]p2p.ID{1: "P1", 3: "P1"},
received: map[int64]p2p.ID{2: "P1", 4: "P1"},
},
args: args{peerID: "P1"},
wantFields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 4, state: peerStateRemoved}},
allB: []int64{},
pending: map[int64]p2p.ID{},
received: map[int64]p2p.ID{},
},
},
{
name: "remove Ready peer from multiple peers set, with blocks received and requested (not yet received)",
fields: scTestParams{
peers: map[string]*scPeer{
"P1": {height: 6, state: peerStateReady},
"P2": {height: 6, state: peerStateReady},
},
allB: []int64{1, 2, 3, 4, 5, 6},
pending: map[int64]p2p.ID{1: "P1", 3: "P2", 6: "P1"},
received: map[int64]p2p.ID{2: "P1", 4: "P2", 5: "P2"},
},
args: args{peerID: "P1"},
wantFields: scTestParams{
peers: map[string]*scPeer{
"P1": {height: 6, state: peerStateRemoved},
"P2": {height: 6, state: peerStateReady},
},
allB: []int64{1, 2, 3, 4, 5, 6},
pending: map[int64]p2p.ID{3: "P2"},
received: map[int64]p2p.ID{4: "P2", 5: "P2"},
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
sc := newTestScheduler(tt.fields)
if err := sc.removePeer(tt.args.peerID); (err != nil) != tt.wantErr {
t.Errorf("removePeer() wantErr %v, error = %v", tt.wantErr, err)
}
wantSc := newTestScheduler(tt.wantFields)
assert.Equal(t, wantSc, sc, "wanted peers %v, got %v", wantSc.peers, sc.peers)
})
}
}
func TestScSetPeerHeight(t *testing.T) {
type args struct {
peerID p2p.ID
height int64
}
tests := []struct {
name string
fields scTestParams
args args
wantFields scTestParams
wantErr bool
}{
{
name: "change height of non existing peer",
fields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}},
allB: []int64{1, 2}},
args: args{peerID: "P2", height: 4},
wantFields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}},
allB: []int64{1, 2}},
wantErr: true,
},
{
name: "increase height of removed peer",
fields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 2, state: peerStateRemoved}}},
args: args{peerID: "P1", height: 4},
wantFields: scTestParams{peers: map[string]*scPeer{"P1": {height: 2, state: peerStateRemoved}}},
wantErr: true,
},
{
name: "decrease height of single peer",
fields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
allB: []int64{1, 2, 3, 4}},
args: args{peerID: "P1", height: 2},
wantFields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
allB: []int64{1, 2, 3, 4}},
wantErr: true,
},
{
name: "increase height of single peer",
fields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}},
allB: []int64{1, 2}},
args: args{peerID: "P1", height: 4},
wantFields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
allB: []int64{1, 2, 3, 4}},
},
{
name: "noop height change of single peer",
fields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
allB: []int64{1, 2, 3, 4}},
args: args{peerID: "P1", height: 4},
wantFields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
allB: []int64{1, 2, 3, 4}},
},
{
name: "add peer with huge height 10**10 ",
fields: scTestParams{
peers: map[string]*scPeer{"P2": {height: -1, state: peerStateNew}},
targetPending: 4,
},
args: args{peerID: "P2", height: 10000000000},
wantFields: scTestParams{
targetPending: 4,
peers: map[string]*scPeer{"P2": {height: 10000000000, state: peerStateReady}},
allB: []int64{1, 2, 3, 4}},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
sc := newTestScheduler(tt.fields)
if err := sc.setPeerHeight(tt.args.peerID, tt.args.height); (err != nil) != tt.wantErr {
t.Errorf("setPeerHeight() wantErr %v, error = %v", tt.wantErr, err)
}
wantSc := newTestScheduler(tt.wantFields)
assert.Equal(t, wantSc, sc, "wanted peers %v, got %v", wantSc.peers, sc.peers)
})
}
}
func TestScGetPeersAtHeight(t *testing.T) {
type args struct {
height int64
}
tests := []struct {
name string
fields scTestParams
args args
wantResult []p2p.ID
}{
{
name: "no peers",
fields: scTestParams{peers: map[string]*scPeer{}},
args: args{height: 10},
wantResult: []p2p.ID{},
},
{
name: "only new peers",
fields: scTestParams{peers: map[string]*scPeer{"P1": {height: -1, state: peerStateNew}}},
args: args{height: 10},
wantResult: []p2p.ID{},
},
{
name: "only Removed peers",
fields: scTestParams{peers: map[string]*scPeer{"P1": {height: 4, state: peerStateRemoved}}},
args: args{height: 2},
wantResult: []p2p.ID{},
},
{
name: "one Ready shorter peer",
fields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
allB: []int64{1, 2, 3, 4},
},
args: args{height: 5},
wantResult: []p2p.ID{},
},
{
name: "one Ready equal peer",
fields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
allB: []int64{1, 2, 3, 4},
},
args: args{height: 4},
wantResult: []p2p.ID{"P1"},
},
{
name: "one Ready higher peer",
fields: scTestParams{
targetPending: 4,
peers: map[string]*scPeer{"P1": {height: 20, state: peerStateReady}},
allB: []int64{1, 2, 3, 4},
},
args: args{height: 4},
wantResult: []p2p.ID{"P1"},
},
{
name: "multiple mixed peers",
fields: scTestParams{
height: 8,
peers: map[string]*scPeer{
"P1": {height: -1, state: peerStateNew},
"P2": {height: 10, state: peerStateReady},
"P3": {height: 5, state: peerStateReady},
"P4": {height: 20, state: peerStateRemoved},
"P5": {height: 11, state: peerStateReady}},
allB: []int64{8, 9, 10, 11},
},
args: args{height: 8},
wantResult: []p2p.ID{"P2", "P5"},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
sc := newTestScheduler(tt.fields)
// getPeersAtHeightOrAbove should not mutate the scheduler
wantSc := sc
res := sc.getPeersAtHeightOrAbove(tt.args.height)
sort.Sort(PeerByID(res))
assert.Equal(t, tt.wantResult, res)
assert.Equal(t, wantSc, sc)
})
}
}
func TestScMarkPending(t *testing.T) {
now := time.Now()
type args struct {
peerID p2p.ID
height int64
tm time.Time
}
tests := []struct {
name string
fields scTestParams
args args
wantFields scTestParams
wantErr bool
}{
{
name: "attempt mark pending an unknown block",
fields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}},
allB: []int64{1, 2}},
args: args{peerID: "P1", height: 3, tm: now},
wantFields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}},
allB: []int64{1, 2}},
wantErr: true,
},
{
name: "attempt mark pending from non existing peer",
fields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}},
allB: []int64{1, 2}},
args: args{peerID: "P2", height: 1, tm: now},
wantFields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}},
allB: []int64{1, 2}},
wantErr: true,
},
{
name: "mark pending from Removed peer",
fields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 2, state: peerStateRemoved}}},
args: args{peerID: "P1", height: 1, tm: now},
wantFields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 2, state: peerStateRemoved}}},
wantErr: true,
},
{
name: "mark pending from New peer",
fields: scTestParams{
peers: map[string]*scPeer{
"P1": {height: 4, state: peerStateReady},
"P2": {height: 4, state: peerStateNew},
},
allB: []int64{1, 2, 3, 4},
},
args: args{peerID: "P2", height: 2, tm: now},
wantFields: scTestParams{
peers: map[string]*scPeer{
"P1": {height: 4, state: peerStateReady},
"P2": {height: 4, state: peerStateNew},
},
allB: []int64{1, 2, 3, 4},
},
wantErr: true,
},
{
name: "mark pending from short peer",
fields: scTestParams{
peers: map[string]*scPeer{
"P1": {height: 4, state: peerStateReady},
"P2": {height: 2, state: peerStateReady},
},
allB: []int64{1, 2, 3, 4},
},
args: args{peerID: "P2", height: 3, tm: now},
wantFields: scTestParams{
peers: map[string]*scPeer{
"P1": {height: 4, state: peerStateReady},
"P2": {height: 2, state: peerStateReady},
},
allB: []int64{1, 2, 3, 4},
},
wantErr: true,
},
{
name: "mark pending all good",
fields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}},
allB: []int64{1, 2},
pending: map[int64]p2p.ID{1: "P1"},
pendingTime: map[int64]time.Time{1: now},
},
args: args{peerID: "P1", height: 2, tm: now.Add(time.Millisecond)},
wantFields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}},
allB: []int64{1, 2},
pending: map[int64]p2p.ID{1: "P1", 2: "P1"},
pendingTime: map[int64]time.Time{1: now, 2: now.Add(time.Millisecond)},
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
sc := newTestScheduler(tt.fields)
if err := sc.markPending(tt.args.peerID, tt.args.height, tt.args.tm); (err != nil) != tt.wantErr {
t.Errorf("markPending() wantErr %v, error = %v", tt.wantErr, err)
}
wantSc := newTestScheduler(tt.wantFields)
assert.Equal(t, wantSc, sc)
})
}
}
func TestScMarkReceived(t *testing.T) {
now := time.Now()
type args struct {
peerID p2p.ID
height int64
size int64
tm time.Time
}
tests := []struct {
name string
fields scTestParams
args args
wantFields scTestParams
wantErr bool
}{
{
name: "received from non existing peer",
fields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}},
allB: []int64{1, 2}},
args: args{peerID: "P2", height: 1, size: 1000, tm: now},
wantFields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}},
allB: []int64{1, 2}},
wantErr: true,
},
{
name: "received from removed peer",
fields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 2, state: peerStateRemoved}}},
args: args{peerID: "P1", height: 1, size: 1000, tm: now},
wantFields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 2, state: peerStateRemoved}}},
wantErr: true,
},
{
name: "received from unsolicited peer",
fields: scTestParams{
peers: map[string]*scPeer{
"P1": {height: 4, state: peerStateReady},
"P2": {height: 4, state: peerStateReady},
},
allB: []int64{1, 2, 3, 4},
pending: map[int64]p2p.ID{1: "P1", 2: "P2", 3: "P2", 4: "P1"},
},
args: args{peerID: "P1", height: 2, size: 1000, tm: now},
wantFields: scTestParams{
peers: map[string]*scPeer{
"P1": {height: 4, state: peerStateReady},
"P2": {height: 4, state: peerStateReady},
},
allB: []int64{1, 2, 3, 4},
pending: map[int64]p2p.ID{1: "P1", 2: "P2", 3: "P2", 4: "P1"},
},
wantErr: true,
},
{
name: "received but blockRequest not sent",
fields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
allB: []int64{1, 2, 3, 4},
pending: map[int64]p2p.ID{},
},
args: args{peerID: "P1", height: 2, size: 1000, tm: now},
wantFields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
allB: []int64{1, 2, 3, 4},
pending: map[int64]p2p.ID{},
},
wantErr: true,
},
{
name: "received with bad timestamp",
fields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}},
allB: []int64{1, 2},
pending: map[int64]p2p.ID{1: "P1", 2: "P1"},
pendingTime: map[int64]time.Time{1: now, 2: now.Add(time.Second)},
},
args: args{peerID: "P1", height: 2, size: 1000, tm: now},
wantFields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}},
allB: []int64{1, 2},
pending: map[int64]p2p.ID{1: "P1", 2: "P1"},
pendingTime: map[int64]time.Time{1: now, 2: now.Add(time.Second)},
},
wantErr: true,
},
{
name: "received all good",
fields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}},
allB: []int64{1, 2},
pending: map[int64]p2p.ID{1: "P1", 2: "P1"},
pendingTime: map[int64]time.Time{1: now, 2: now},
},
args: args{peerID: "P1", height: 2, size: 1000, tm: now.Add(time.Millisecond)},
wantFields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}},
allB: []int64{1, 2},
pending: map[int64]p2p.ID{1: "P1"},
pendingTime: map[int64]time.Time{1: now},
received: map[int64]p2p.ID{2: "P1"},
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
sc := newTestScheduler(tt.fields)
if err := sc.markReceived(tt.args.peerID,
tt.args.height, tt.args.size, now.Add(time.Second)); (err != nil) != tt.wantErr {
t.Errorf("markReceived() wantErr %v, error = %v", tt.wantErr, err)
}
wantSc := newTestScheduler(tt.wantFields)
assert.Equal(t, wantSc, sc)
})
}
}
func TestScMarkProcessed(t *testing.T) {
now := time.Now()
type args struct {
height int64
}
tests := []struct {
name string
fields scTestParams
args args
wantFields scTestParams
wantErr bool
}{
{
name: "processed an unreceived block",
fields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}},
allB: []int64{1, 2},
pending: map[int64]p2p.ID{2: "P1"},
pendingTime: map[int64]time.Time{2: now},
received: map[int64]p2p.ID{1: "P1"}},
args: args{height: 2},
wantFields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}},
allB: []int64{1, 2},
pending: map[int64]p2p.ID{2: "P1"},
pendingTime: map[int64]time.Time{2: now},
received: map[int64]p2p.ID{1: "P1"}},
wantErr: true,
},
{
name: "mark processed success",
fields: scTestParams{
height: 1,
peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}},
allB: []int64{1, 2},
pending: map[int64]p2p.ID{2: "P1"},
pendingTime: map[int64]time.Time{2: now},
received: map[int64]p2p.ID{1: "P1"}},
args: args{height: 1},
wantFields: scTestParams{
height: 2,
peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}},
allB: []int64{2},
pending: map[int64]p2p.ID{2: "P1"},
pendingTime: map[int64]time.Time{2: now}},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
sc := newTestScheduler(tt.fields)
if err := sc.markProcessed(tt.args.height); (err != nil) != tt.wantErr {
t.Errorf("markProcessed() wantErr %v, error = %v", tt.wantErr, err)
}
wantSc := newTestScheduler(tt.wantFields)
assert.Equal(t, wantSc, sc)
})
}
}
func TestScAllBlocksProcessed(t *testing.T) {
now := time.Now()
tests := []struct {
name string
fields scTestParams
wantResult bool
}{
{
name: "no blocks",
fields: scTestParams{},
wantResult: true,
},
{
name: "only New blocks",
fields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
allB: []int64{1, 2, 3, 4},
},
wantResult: false,
},
{
name: "only Pending blocks",
fields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
allB: []int64{1, 2, 3, 4},
pending: map[int64]p2p.ID{1: "P1", 2: "P1", 3: "P1", 4: "P1"},
pendingTime: map[int64]time.Time{1: now, 2: now, 3: now, 4: now},
},
wantResult: false,
},
{
name: "only Received blocks",
fields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
allB: []int64{1, 2, 3, 4},
received: map[int64]p2p.ID{1: "P1", 2: "P1", 3: "P1", 4: "P1"},
},
wantResult: false,
},
{
name: "only Processed blocks plus highest is received",
fields: scTestParams{
height: 4,
peers: map[string]*scPeer{
"P1": {height: 4, state: peerStateReady}},
allB: []int64{4},
received: map[int64]p2p.ID{4: "P1"},
},
wantResult: true,
},
{
name: "mixed block states",
fields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
allB: []int64{1, 2, 3, 4},
pending: map[int64]p2p.ID{2: "P1", 4: "P1"},
pendingTime: map[int64]time.Time{2: now, 4: now},
},
wantResult: false,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
sc := newTestScheduler(tt.fields)
// allBlocksProcessed() should not mutate the scheduler
wantSc := sc
res := sc.allBlocksProcessed()
assert.Equal(t, tt.wantResult, res)
assert.Equal(t, wantSc, sc)
})
}
}
func TestScNextHeightToSchedule(t *testing.T) {
now := time.Now()
tests := []struct {
name string
fields scTestParams
wantHeight int64
}{
{
name: "no blocks",
fields: scTestParams{initHeight: 10, height: 11},
wantHeight: -1,
},
{
name: "only New blocks",
fields: scTestParams{
initHeight: 2,
height: 3,
peers: map[string]*scPeer{"P1": {height: 6, state: peerStateReady}},
allB: []int64{3, 4, 5, 6},
},
wantHeight: 3,
},
{
name: "only Pending blocks",
fields: scTestParams{
height: 1,
peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
allB: []int64{1, 2, 3, 4},
pending: map[int64]p2p.ID{1: "P1", 2: "P1", 3: "P1", 4: "P1"},
pendingTime: map[int64]time.Time{1: now, 2: now, 3: now, 4: now},
},
wantHeight: -1,
},
{
name: "only Received blocks",
fields: scTestParams{
height: 1,
peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
allB: []int64{1, 2, 3, 4},
received: map[int64]p2p.ID{1: "P1", 2: "P1", 3: "P1", 4: "P1"},
},
wantHeight: -1,
},
{
name: "only Processed blocks",
fields: scTestParams{
height: 1,
peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
allB: []int64{1, 2, 3, 4},
},
wantHeight: 1,
},
{
name: "mixed block states",
fields: scTestParams{
height: 1,
peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
allB: []int64{1, 2, 3, 4},
pending: map[int64]p2p.ID{2: "P1"},
pendingTime: map[int64]time.Time{2: now},
},
wantHeight: 1,
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
sc := newTestScheduler(tt.fields)
// nextHeightToSchedule() should not mutate the scheduler
wantSc := sc
resMin := sc.nextHeightToSchedule()
assert.Equal(t, tt.wantHeight, resMin)
assert.Equal(t, wantSc, sc)
})
}
}
func TestScSelectPeer(t *testing.T) {
type args struct {
height int64
}
tests := []struct {
name string
fields scTestParams
args args
wantResult p2p.ID
wantError bool
}{
{
name: "no peers",
fields: scTestParams{peers: map[string]*scPeer{}},
args: args{height: 10},
wantResult: "",
wantError: true,
},
{
name: "only new peers",
fields: scTestParams{peers: map[string]*scPeer{"P1": {height: -1, state: peerStateNew}}},
args: args{height: 10},
wantResult: "",
wantError: true,
},
{
name: "only Removed peers",
fields: scTestParams{peers: map[string]*scPeer{"P1": {height: 4, state: peerStateRemoved}}},
args: args{height: 2},
wantResult: "",
wantError: true,
},
{
name: "one Ready shorter peer",
fields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
allB: []int64{1, 2, 3, 4},
},
args: args{height: 5},
wantResult: "",
wantError: true,
},
{
name: "one Ready equal peer",
fields: scTestParams{peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
allB: []int64{1, 2, 3, 4},
},
args: args{height: 4},
wantResult: "P1",
},
{
name: "one Ready higher peer",
fields: scTestParams{peers: map[string]*scPeer{"P1": {height: 6, state: peerStateReady}},
allB: []int64{1, 2, 3, 4, 5, 6},
},
args: args{height: 4},
wantResult: "P1",
},
{
name: "many Ready higher peers with different number of pending requests",
fields: scTestParams{
height: 4,
peers: map[string]*scPeer{
"P1": {height: 8, state: peerStateReady},
"P2": {height: 9, state: peerStateReady}},
allB: []int64{4, 5, 6, 7, 8, 9},
pending: map[int64]p2p.ID{
4: "P1", 6: "P1",
5: "P2",
},
},
args: args{height: 4},
wantResult: "P2",
},
{
name: "many Ready higher peers with same number of pending requests",
fields: scTestParams{
peers: map[string]*scPeer{
"P2": {height: 20, state: peerStateReady},
"P1": {height: 15, state: peerStateReady},
"P3": {height: 15, state: peerStateReady}},
allB: []int64{1, 2, 3, 4, 5, 6, 7, 8, 9, 10},
pending: map[int64]p2p.ID{
1: "P1", 2: "P1",
3: "P3", 4: "P3",
5: "P2", 6: "P2",
},
},
args: args{height: 7},
wantResult: "P1",
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
sc := newTestScheduler(tt.fields)
// selectPeer should not mutate the scheduler
wantSc := sc
res, err := sc.selectPeer(tt.args.height)
assert.Equal(t, tt.wantResult, res)
assert.Equal(t, tt.wantError, err != nil)
assert.Equal(t, wantSc, sc)
})
}
}
// makeScBlock makes an empty block.
func makeScBlock(height int64) *types.Block {
return &types.Block{Header: types.Header{Height: height}}
}
// checkScResults checks scheduler handler test results
func checkScResults(t *testing.T, wantErr bool, err error, wantEvent Event, event Event) {
if (err != nil) != wantErr {
t.Errorf("error = %v, wantErr %v", err, wantErr)
return
}
switch wantEvent := wantEvent.(type) {
case scPeerError:
assert.Equal(t, wantEvent.peerID, event.(scPeerError).peerID)
assert.Equal(t, wantEvent.reason != nil, event.(scPeerError).reason != nil)
case scBlockReceived:
assert.Equal(t, wantEvent.peerID, event.(scBlockReceived).peerID)
assert.Equal(t, wantEvent.block, event.(scBlockReceived).block)
case scSchedulerFail:
assert.Equal(t, wantEvent.reason != nil, event.(scSchedulerFail).reason != nil)
default:
assert.Equal(t, wantEvent, event)
}
}
func TestScHandleBlockResponse(t *testing.T) {
now := time.Now()
block6FromP1 := bcBlockResponse{
time: now.Add(time.Millisecond),
peerID: p2p.ID("P1"),
height: 6,
size: 100,
block: makeScBlock(6),
}
type args struct {
event bcBlockResponse
}
tests := []struct {
name string
fields scTestParams
args args
wantEvent Event
wantErr bool
}{
{
name: "empty scheduler",
fields: scTestParams{},
args: args{event: block6FromP1},
wantEvent: scPeerError{peerID: "P1", reason: fmt.Errorf("some error")},
},
{
name: "block from removed peer",
fields: scTestParams{peers: map[string]*scPeer{"P1": {height: 8, state: peerStateRemoved}}},
args: args{event: block6FromP1},
wantEvent: scPeerError{peerID: "P1", reason: fmt.Errorf("some error")},
},
{
name: "block we haven't asked for",
fields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 8, state: peerStateReady}},
allB: []int64{1, 2, 3, 4, 5, 6, 7, 8}},
args: args{event: block6FromP1},
wantEvent: scPeerError{peerID: "P1", reason: fmt.Errorf("some error")},
},
{
name: "block from wrong peer",
fields: scTestParams{
peers: map[string]*scPeer{"P2": {height: 8, state: peerStateReady}},
allB: []int64{1, 2, 3, 4, 5, 6, 7, 8},
pending: map[int64]p2p.ID{6: "P2"},
pendingTime: map[int64]time.Time{6: now},
},
args: args{event: block6FromP1},
wantEvent: scPeerError{peerID: "P1", reason: fmt.Errorf("some error")},
},
{
name: "block with bad timestamp",
fields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 8, state: peerStateReady}},
allB: []int64{1, 2, 3, 4, 5, 6, 7, 8},
pending: map[int64]p2p.ID{6: "P1"},
pendingTime: map[int64]time.Time{6: now.Add(time.Second)},
},
args: args{event: block6FromP1},
wantEvent: scPeerError{peerID: "P1", reason: fmt.Errorf("some error")},
},
{
name: "good block, accept",
fields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 8, state: peerStateReady}},
allB: []int64{1, 2, 3, 4, 5, 6, 7, 8},
pending: map[int64]p2p.ID{6: "P1"},
pendingTime: map[int64]time.Time{6: now},
},
args: args{event: block6FromP1},
wantEvent: scBlockReceived{peerID: "P1", block: makeScBlock(6)},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
sc := newTestScheduler(tt.fields)
event, err := sc.handleBlockResponse(tt.args.event)
checkScResults(t, tt.wantErr, err, tt.wantEvent, event)
})
}
}
func TestScHandleBlockProcessed(t *testing.T) {
now := time.Now()
processed6FromP1 := pcBlockProcessed{
peerID: p2p.ID("P1"),
height: 6,
}
type args struct {
event pcBlockProcessed
}
tests := []struct {
name string
fields scTestParams
args args
wantEvent Event
wantErr bool
}{
{
name: "empty scheduler",
fields: scTestParams{height: 6},
args: args{event: processed6FromP1},
wantEvent: scSchedulerFail{reason: fmt.Errorf("some error")},
},
{
name: "processed block we don't have",
fields: scTestParams{
initHeight: 5,
height: 6,
peers: map[string]*scPeer{"P1": {height: 8, state: peerStateReady}},
allB: []int64{6, 7, 8},
pending: map[int64]p2p.ID{6: "P1"},
pendingTime: map[int64]time.Time{6: now},
},
args: args{event: processed6FromP1},
wantEvent: scSchedulerFail{reason: fmt.Errorf("some error")},
},
{
name: "processed block ok, we processed all blocks",
fields: scTestParams{
initHeight: 5,
height: 6,
peers: map[string]*scPeer{"P1": {height: 7, state: peerStateReady}},
allB: []int64{6, 7},
received: map[int64]p2p.ID{6: "P1", 7: "P1"},
},
args: args{event: processed6FromP1},
wantEvent: scFinishedEv{},
},
{
name: "processed block ok, we still have blocks to process",
fields: scTestParams{
initHeight: 5,
height: 6,
peers: map[string]*scPeer{"P1": {height: 8, state: peerStateReady}},
allB: []int64{6, 7, 8},
pending: map[int64]p2p.ID{7: "P1", 8: "P1"},
received: map[int64]p2p.ID{6: "P1"},
},
args: args{event: processed6FromP1},
wantEvent: noOpEvent{},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
sc := newTestScheduler(tt.fields)
event, err := sc.handleBlockProcessed(tt.args.event)
checkScResults(t, tt.wantErr, err, tt.wantEvent, event)
})
}
}
func TestScHandleBlockVerificationFailure(t *testing.T) {
now := time.Now()
type args struct {
event pcBlockVerificationFailure
}
tests := []struct {
name string
fields scTestParams
args args
wantEvent Event
wantErr bool
}{
{
name: "empty scheduler",
fields: scTestParams{},
args: args{event: pcBlockVerificationFailure{height: 10, firstPeerID: "P1", secondPeerID: "P1"}},
wantEvent: noOpEvent{},
},
{
name: "failed block we don't have, single peer is still removed",
fields: scTestParams{
initHeight: 5,
height: 6,
peers: map[string]*scPeer{"P1": {height: 8, state: peerStateReady}},
allB: []int64{6, 7, 8},
pending: map[int64]p2p.ID{6: "P1"},
pendingTime: map[int64]time.Time{6: now},
},
args: args{event: pcBlockVerificationFailure{height: 10, firstPeerID: "P1", secondPeerID: "P1"}},
wantEvent: scFinishedEv{},
},
{
name: "failed block we don't have, one of two peers are removed",
fields: scTestParams{
initHeight: 5,
peers: map[string]*scPeer{"P1": {height: 8, state: peerStateReady}, "P2": {height: 8, state: peerStateReady}},
allB: []int64{6, 7, 8},
pending: map[int64]p2p.ID{6: "P1"},
pendingTime: map[int64]time.Time{6: now},
},
args: args{event: pcBlockVerificationFailure{height: 10, firstPeerID: "P1", secondPeerID: "P1"}},
wantEvent: noOpEvent{},
},
{
name: "failed block, all blocks are processed after removal",
fields: scTestParams{
initHeight: 5,
height: 6,
peers: map[string]*scPeer{"P1": {height: 7, state: peerStateReady}},
allB: []int64{6, 7},
received: map[int64]p2p.ID{6: "P1", 7: "P1"},
},
args: args{event: pcBlockVerificationFailure{height: 7, firstPeerID: "P1", secondPeerID: "P1"}},
wantEvent: scFinishedEv{},
},
{
name: "failed block, we still have blocks to process",
fields: scTestParams{
initHeight: 4,
peers: map[string]*scPeer{"P1": {height: 8, state: peerStateReady}, "P2": {height: 8, state: peerStateReady}},
allB: []int64{5, 6, 7, 8},
pending: map[int64]p2p.ID{7: "P1", 8: "P1"},
received: map[int64]p2p.ID{5: "P1", 6: "P1"},
},
args: args{event: pcBlockVerificationFailure{height: 5, firstPeerID: "P1", secondPeerID: "P1"}},
wantEvent: noOpEvent{},
},
{
name: "failed block, H+1 and H+2 delivered by different peers, we still have blocks to process",
fields: scTestParams{
initHeight: 4,
peers: map[string]*scPeer{
"P1": {height: 8, state: peerStateReady},
"P2": {height: 8, state: peerStateReady},
"P3": {height: 8, state: peerStateReady},
},
allB: []int64{5, 6, 7, 8},
pending: map[int64]p2p.ID{7: "P1", 8: "P1"},
received: map[int64]p2p.ID{5: "P1", 6: "P1"},
},
args: args{event: pcBlockVerificationFailure{height: 5, firstPeerID: "P1", secondPeerID: "P2"}},
wantEvent: noOpEvent{},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
sc := newTestScheduler(tt.fields)
event, err := sc.handleBlockProcessError(tt.args.event)
checkScResults(t, tt.wantErr, err, tt.wantEvent, event)
})
}
}
func TestScHandleAddNewPeer(t *testing.T) {
addP1 := addNewPeer{
peerID: p2p.ID("P1"),
}
type args struct {
event addNewPeer
}
tests := []struct {
name string
fields scTestParams
args args
wantEvent Event
wantErr bool
}{
{
name: "add P1 to empty scheduler",
fields: scTestParams{},
args: args{event: addP1},
wantEvent: noOpEvent{},
},
{
name: "add duplicate peer",
fields: scTestParams{
height: 6,
peers: map[string]*scPeer{"P1": {height: 8, state: peerStateReady}},
allB: []int64{6, 7, 8},
},
args: args{event: addP1},
wantEvent: scSchedulerFail{reason: fmt.Errorf("some error")},
},
{
name: "add P1 to non empty scheduler",
fields: scTestParams{
height: 6,
peers: map[string]*scPeer{"P2": {height: 8, state: peerStateReady}},
allB: []int64{6, 7, 8},
},
args: args{event: addP1},
wantEvent: noOpEvent{},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
sc := newTestScheduler(tt.fields)
event, err := sc.handleAddNewPeer(tt.args.event)
checkScResults(t, tt.wantErr, err, tt.wantEvent, event)
})
}
}
func TestScHandlePeerError(t *testing.T) {
errP1 := peerError{
peerID: p2p.ID("P1"),
}
type args struct {
event peerError
}
tests := []struct {
name string
fields scTestParams
args args
wantEvent Event
wantErr bool
}{
{
name: "no peers",
fields: scTestParams{},
args: args{event: errP1},
wantEvent: scSchedulerFail{reason: fmt.Errorf("some error")},
},
{
name: "error finds no peer",
fields: scTestParams{
height: 6,
peers: map[string]*scPeer{"P2": {height: 8, state: peerStateReady}},
allB: []int64{6, 7, 8},
},
args: args{event: errP1},
wantEvent: scSchedulerFail{reason: fmt.Errorf("some error")},
},
{
name: "error finds peer, only peer is removed",
fields: scTestParams{
height: 6,
peers: map[string]*scPeer{"P1": {height: 8, state: peerStateReady}},
allB: []int64{6, 7, 8},
},
args: args{event: errP1},
wantEvent: scFinishedEv{},
},
{
name: "error finds peer, one of two peers are removed",
fields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 8, state: peerStateReady}, "P2": {height: 8, state: peerStateReady}},
allB: []int64{1, 2, 3, 4, 5, 6, 7, 8},
},
args: args{event: errP1},
wantEvent: noOpEvent{},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
sc := newTestScheduler(tt.fields)
event, err := sc.handlePeerError(tt.args.event)
checkScResults(t, tt.wantErr, err, tt.wantEvent, event)
})
}
}
func TestScHandleTryPrunePeer(t *testing.T) {
now := time.Now()
pruneEv := tryPrunePeer{
time: now.Add(time.Second + time.Millisecond),
}
type args struct {
event tryPrunePeer
}
tests := []struct {
name string
fields scTestParams
args args
wantEvent Event
wantErr bool
}{
{
name: "no peers",
fields: scTestParams{},
args: args{event: pruneEv},
wantEvent: noOpEvent{},
},
{
name: "no prunable peers",
fields: scTestParams{
minRecvRate: 100,
peers: map[string]*scPeer{
// X - removed, active, fast
"P1": {state: peerStateRemoved, lastTouched: now.Add(time.Second), lastRate: 101},
// X - ready, active, fast
"P2": {state: peerStateReady, lastTouched: now.Add(time.Second), lastRate: 101},
// X - removed, active, equal
"P3": {state: peerStateRemoved, lastTouched: now.Add(time.Second), lastRate: 100}},
peerTimeout: time.Second,
},
args: args{event: pruneEv},
wantEvent: noOpEvent{},
},
{
name: "mixed peers",
fields: scTestParams{
minRecvRate: 100,
peers: map[string]*scPeer{
// X - removed, active, fast
"P1": {state: peerStateRemoved, lastTouched: now.Add(time.Second), lastRate: 101, height: 5},
// X - ready, active, fast
"P2": {state: peerStateReady, lastTouched: now.Add(time.Second), lastRate: 101, height: 5},
// X - removed, active, equal
"P3": {state: peerStateRemoved, lastTouched: now.Add(time.Second), lastRate: 100, height: 5},
// V - ready, inactive, equal
"P4": {state: peerStateReady, lastTouched: now, lastRate: 100, height: 7},
// V - ready, inactive, slow
"P5": {state: peerStateReady, lastTouched: now, lastRate: 99, height: 7},
// V - ready, active, slow
"P6": {state: peerStateReady, lastTouched: now.Add(time.Second), lastRate: 90, height: 7},
},
allB: []int64{1, 2, 3, 4, 5, 6, 7},
peerTimeout: time.Second},
args: args{event: pruneEv},
wantEvent: scPeersPruned{peers: []p2p.ID{"P4", "P5", "P6"}},
},
{
name: "mixed peers, finish after pruning",
fields: scTestParams{
minRecvRate: 100,
height: 6,
peers: map[string]*scPeer{
// X - removed, active, fast
"P1": {state: peerStateRemoved, lastTouched: now.Add(time.Second), lastRate: 101, height: 5},
// X - ready, active, fast
"P2": {state: peerStateReady, lastTouched: now.Add(time.Second), lastRate: 101, height: 5},
// X - removed, active, equal
"P3": {state: peerStateRemoved, lastTouched: now.Add(time.Second), lastRate: 100, height: 5},
// V - ready, inactive, equal
"P4": {state: peerStateReady, lastTouched: now, lastRate: 100, height: 7},
// V - ready, inactive, slow
"P5": {state: peerStateReady, lastTouched: now, lastRate: 99, height: 7},
// V - ready, active, slow
"P6": {state: peerStateReady, lastTouched: now.Add(time.Second), lastRate: 90, height: 7},
},
allB: []int64{6, 7},
peerTimeout: time.Second},
args: args{event: pruneEv},
wantEvent: scFinishedEv{},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
sc := newTestScheduler(tt.fields)
event, err := sc.handleTryPrunePeer(tt.args.event)
checkScResults(t, tt.wantErr, err, tt.wantEvent, event)
})
}
}
func TestHandleTrySchedule(t *testing.T) {
now := time.Now()
tryEv := trySchedule{
time: now.Add(time.Second + time.Millisecond),
}
type args struct {
event trySchedule
}
tests := []struct {
name string
fields scTestParams
args args
wantEvent Event
wantErr bool
}{
{
name: "no peers",
fields: scTestParams{peers: map[string]*scPeer{}},
args: args{event: tryEv},
wantEvent: noOpEvent{},
},
{
name: "only new peers",
fields: scTestParams{peers: map[string]*scPeer{"P1": {height: -1, state: peerStateNew}}},
args: args{event: tryEv},
wantEvent: noOpEvent{},
},
{
name: "only Removed peers",
fields: scTestParams{peers: map[string]*scPeer{"P1": {height: 4, state: peerStateRemoved}}},
args: args{event: tryEv},
wantEvent: noOpEvent{},
},
{
name: "one Ready shorter peer",
fields: scTestParams{
height: 6,
peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}}},
args: args{event: tryEv},
wantEvent: noOpEvent{},
},
{
name: "one Ready equal peer",
fields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
allB: []int64{1, 2, 3, 4}},
args: args{event: tryEv},
wantEvent: scBlockRequest{peerID: "P1", height: 1},
},
{
name: "many Ready higher peers with different number of pending requests",
fields: scTestParams{
peers: map[string]*scPeer{
"P1": {height: 4, state: peerStateReady},
"P2": {height: 5, state: peerStateReady}},
allB: []int64{1, 2, 3, 4, 5},
pending: map[int64]p2p.ID{
1: "P1", 2: "P1",
3: "P2",
},
},
args: args{event: tryEv},
wantEvent: scBlockRequest{peerID: "P2", height: 4},
},
{
name: "many Ready higher peers with same number of pending requests",
fields: scTestParams{
peers: map[string]*scPeer{
"P2": {height: 8, state: peerStateReady},
"P1": {height: 8, state: peerStateReady},
"P3": {height: 8, state: peerStateReady}},
allB: []int64{1, 2, 3, 4, 5, 6, 7, 8},
pending: map[int64]p2p.ID{
1: "P1", 2: "P1",
3: "P3", 4: "P3",
5: "P2", 6: "P2",
},
},
args: args{event: tryEv},
wantEvent: scBlockRequest{peerID: "P1", height: 7},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
sc := newTestScheduler(tt.fields)
event, err := sc.handleTrySchedule(tt.args.event)
checkScResults(t, tt.wantErr, err, tt.wantEvent, event)
})
}
}
func TestScHandleStatusResponse(t *testing.T) {
now := time.Now()
statusRespP1Ev := bcStatusResponse{
time: now.Add(time.Second + time.Millisecond),
peerID: "P1",
height: 6,
}
type args struct {
event bcStatusResponse
}
tests := []struct {
name string
fields scTestParams
args args
wantEvent Event
wantErr bool
}{
{
name: "change height of non existing peer",
fields: scTestParams{
peers: map[string]*scPeer{"P2": {height: 2, state: peerStateReady}},
allB: []int64{1, 2},
},
args: args{event: statusRespP1Ev},
wantEvent: scPeerError{peerID: "P1", reason: fmt.Errorf("some error")},
},
{
name: "increase height of removed peer",
fields: scTestParams{peers: map[string]*scPeer{"P1": {height: 2, state: peerStateRemoved}}},
args: args{event: statusRespP1Ev},
wantEvent: scPeerError{peerID: "P1", reason: fmt.Errorf("some error")},
},
{
name: "decrease height of single peer",
fields: scTestParams{
height: 5,
peers: map[string]*scPeer{"P1": {height: 10, state: peerStateReady}},
allB: []int64{5, 6, 7, 8, 9, 10},
},
args: args{event: statusRespP1Ev},
wantEvent: scPeerError{peerID: "P1", reason: fmt.Errorf("some error")},
},
{
name: "increase height of single peer",
fields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 2, state: peerStateReady}},
allB: []int64{1, 2}},
args: args{event: statusRespP1Ev},
wantEvent: noOpEvent{},
},
{
name: "noop height change of single peer",
fields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 6, state: peerStateReady}},
allB: []int64{1, 2, 3, 4, 5, 6}},
args: args{event: statusRespP1Ev},
wantEvent: noOpEvent{},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
sc := newTestScheduler(tt.fields)
event, err := sc.handleStatusResponse(tt.args.event)
checkScResults(t, tt.wantErr, err, tt.wantEvent, event)
})
}
}
func TestScHandle(t *testing.T) {
type unknownEv struct {
priorityNormal
}
t0 := time.Now()
tick := make([]time.Time, 100)
for i := range tick {
tick[i] = t0.Add(time.Duration(i) * time.Millisecond)
}
type args struct {
event Event
}
type scStep struct {
currentSc *scTestParams
args args
wantEvent Event
wantErr bool
wantSc *scTestParams
}
tests := []struct {
name string
steps []scStep
}{
{
name: "unknown event",
steps: []scStep{
{ // add P1
currentSc: &scTestParams{},
args: args{event: unknownEv{}},
wantEvent: scSchedulerFail{reason: fmt.Errorf("some error")},
wantSc: &scTestParams{},
},
},
},
{
name: "single peer, sync 3 blocks",
steps: []scStep{
{ // add P1
currentSc: &scTestParams{peers: map[string]*scPeer{}, height: 1},
args: args{event: addNewPeer{peerID: "P1"}},
wantEvent: noOpEvent{},
wantSc: &scTestParams{peers: map[string]*scPeer{"P1": {height: -1, state: peerStateNew}}, height: 1},
},
{ // set height of P1
args: args{event: bcStatusResponse{peerID: "P1", time: tick[0], height: 3}},
wantEvent: noOpEvent{},
wantSc: &scTestParams{
peers: map[string]*scPeer{"P1": {height: 3, state: peerStateReady}},
allB: []int64{1, 2, 3},
height: 1,
},
},
{ // schedule block 1
args: args{event: trySchedule{time: tick[1]}},
wantEvent: scBlockRequest{peerID: "P1", height: 1},
wantSc: &scTestParams{
peers: map[string]*scPeer{"P1": {height: 3, state: peerStateReady}},
allB: []int64{1, 2, 3},
pending: map[int64]p2p.ID{1: "P1"},
pendingTime: map[int64]time.Time{1: tick[1]},
height: 1,
},
},
{ // schedule block 2
args: args{event: trySchedule{time: tick[2]}},
wantEvent: scBlockRequest{peerID: "P1", height: 2},
wantSc: &scTestParams{
peers: map[string]*scPeer{"P1": {height: 3, state: peerStateReady}},
allB: []int64{1, 2, 3},
pending: map[int64]p2p.ID{1: "P1", 2: "P1"},
pendingTime: map[int64]time.Time{1: tick[1], 2: tick[2]},
height: 1,
},
},
{ // schedule block 3
args: args{event: trySchedule{time: tick[3]}},
wantEvent: scBlockRequest{peerID: "P1", height: 3},
wantSc: &scTestParams{
peers: map[string]*scPeer{"P1": {height: 3, state: peerStateReady}},
allB: []int64{1, 2, 3},
pending: map[int64]p2p.ID{1: "P1", 2: "P1", 3: "P1"},
pendingTime: map[int64]time.Time{1: tick[1], 2: tick[2], 3: tick[3]},
height: 1,
},
},
{ // block response 1
args: args{event: bcBlockResponse{peerID: "P1", height: 1, time: tick[4], size: 100, block: makeScBlock(1)}},
wantEvent: scBlockReceived{peerID: "P1", block: makeScBlock(1)},
wantSc: &scTestParams{
peers: map[string]*scPeer{"P1": {height: 3, state: peerStateReady, lastTouched: tick[4]}},
allB: []int64{1, 2, 3},
pending: map[int64]p2p.ID{2: "P1", 3: "P1"},
pendingTime: map[int64]time.Time{2: tick[2], 3: tick[3]},
received: map[int64]p2p.ID{1: "P1"},
height: 1,
},
},
{ // block response 2
args: args{event: bcBlockResponse{peerID: "P1", height: 2, time: tick[5], size: 100, block: makeScBlock(2)}},
wantEvent: scBlockReceived{peerID: "P1", block: makeScBlock(2)},
wantSc: &scTestParams{
peers: map[string]*scPeer{"P1": {height: 3, state: peerStateReady, lastTouched: tick[5]}},
allB: []int64{1, 2, 3},
pending: map[int64]p2p.ID{3: "P1"},
pendingTime: map[int64]time.Time{3: tick[3]},
received: map[int64]p2p.ID{1: "P1", 2: "P1"},
height: 1,
},
},
{ // block response 3
args: args{event: bcBlockResponse{peerID: "P1", height: 3, time: tick[6], size: 100, block: makeScBlock(3)}},
wantEvent: scBlockReceived{peerID: "P1", block: makeScBlock(3)},
wantSc: &scTestParams{
peers: map[string]*scPeer{"P1": {height: 3, state: peerStateReady, lastTouched: tick[6]}},
allB: []int64{1, 2, 3},
received: map[int64]p2p.ID{1: "P1", 2: "P1", 3: "P1"},
height: 1,
},
},
{ // processed block 1
args: args{event: pcBlockProcessed{peerID: p2p.ID("P1"), height: 1}},
wantEvent: noOpEvent{},
wantSc: &scTestParams{
peers: map[string]*scPeer{"P1": {height: 3, state: peerStateReady, lastTouched: tick[6]}},
allB: []int64{2, 3},
received: map[int64]p2p.ID{2: "P1", 3: "P1"},
height: 2,
},
},
{ // processed block 2
args: args{event: pcBlockProcessed{peerID: p2p.ID("P1"), height: 2}},
wantEvent: scFinishedEv{},
wantSc: &scTestParams{
peers: map[string]*scPeer{"P1": {height: 3, state: peerStateReady, lastTouched: tick[6]}},
allB: []int64{3},
received: map[int64]p2p.ID{3: "P1"},
height: 3,
},
},
},
},
{
name: "block verification failure",
steps: []scStep{
{ // failure processing block 1
currentSc: &scTestParams{
peers: map[string]*scPeer{
"P1": {height: 4, state: peerStateReady, lastTouched: tick[6]},
"P2": {height: 3, state: peerStateReady, lastTouched: tick[6]}},
allB: []int64{1, 2, 3, 4},
received: map[int64]p2p.ID{1: "P1", 2: "P1", 3: "P1"},
height: 1,
},
args: args{event: pcBlockVerificationFailure{height: 1, firstPeerID: "P1", secondPeerID: "P1"}},
wantEvent: noOpEvent{},
wantSc: &scTestParams{
peers: map[string]*scPeer{
"P1": {height: 4, state: peerStateRemoved, lastTouched: tick[6]},
"P2": {height: 3, state: peerStateReady, lastTouched: tick[6]}},
allB: []int64{1, 2, 3},
received: map[int64]p2p.ID{},
height: 1,
},
},
/*
{ // processed block 2
args: args{event: pcBlockProcessed{peerID: p2p.ID("P1"), height: 2}},
wantEvent: scFinishedEv{},
wantSc: &scTestParams{
peers: map[string]*scPeer{"P1": {height: 3, state: peerStateReady, lastTouched: tick[6]}},
allB: []int64{3},
received: map[int64]p2p.ID{3: "P1"},
height: 3,
},
},
*/
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
var sc *scheduler
for i, step := range tt.steps {
// First step must always initialise the currentState as state.
if step.currentSc != nil {
sc = newTestScheduler(*step.currentSc)
}
if sc == nil {
panic("Bad (initial?) step")
}
nextEvent, err := sc.handle(step.args.event)
assert.Equal(t, newTestScheduler(*step.wantSc), sc)
t.Logf("step %d(%v): %s", i, step.args.event, sc)
checkScResults(t, step.wantErr, err, step.wantEvent, nextEvent)
// Next step may use the wantedState as their currentState.
sc = newTestScheduler(*step.wantSc)
}
})
}
}