You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

2253 lines
65 KiB

package v2
import (
"fmt"
"math"
"sort"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/tendermint/tendermint/p2p"
"github.com/tendermint/tendermint/state"
"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
startTime time.Time
syncTimeout time.Duration
}
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)
var maxHeight int64
initHeight := params.initHeight
if initHeight == 0 {
initHeight = 1
}
sc := newScheduler(initHeight, params.startTime)
if params.height != 0 {
sc.height = params.height
}
for id, peer := range params.peers {
peer.peerID = p2p.ID(id)
peers[p2p.ID(id)] = peer
if maxHeight < peer.height {
maxHeight = peer.height
}
}
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.syncTimeout == 0 {
sc.syncTimeout = 10 * time.Second
} else {
sc.syncTimeout = params.syncTimeout
}
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, time.Now())
)
assert.Equal(t, blockStateProcessed, sc.getStateAtHeight(initHeight-1))
assert.Equal(t, blockStateUnknown, 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{
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": {base: -1, height: -1, state: peerStateNew},
"P2": {base: -1, 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 TestScEnsurePeer(t *testing.T) {
type args struct {
peerID p2p.ID
}
tests := []struct {
name string
fields scTestParams
args args
wantFields scTestParams
}{
{
name: "add first peer",
fields: scTestParams{},
args: args{peerID: "P1"},
wantFields: scTestParams{peers: map[string]*scPeer{"P1": {base: -1, height: -1, state: peerStateNew}}},
},
{
name: "add second peer",
fields: scTestParams{peers: map[string]*scPeer{"P1": {base: -1, height: -1, state: peerStateNew}}},
args: args{peerID: "P2"},
wantFields: scTestParams{peers: map[string]*scPeer{
"P1": {base: -1, height: -1, state: peerStateNew},
"P2": {base: -1, height: -1, state: peerStateNew}}},
},
{
name: "add duplicate peer is fine",
fields: scTestParams{peers: map[string]*scPeer{"P1": {height: -1}}},
args: args{peerID: "P1"},
wantFields: scTestParams{peers: map[string]*scPeer{"P1": {height: -1}}},
},
{
name: "add duplicate peer with existing peer in Ready state is noop",
fields: scTestParams{
peers: map[string]*scPeer{"P1": {state: peerStateReady, height: 3}},
allB: []int64{1, 2, 3},
},
args: args{peerID: "P1"},
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)
sc.ensurePeer(tt.args.peerID)
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 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}}},
},
{
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 TestScSetPeerRange(t *testing.T) {
type args struct {
peerID p2p.ID
base int64
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},
"P2": {height: 4, state: peerStateReady},
},
allB: []int64{1, 2, 3, 4}},
},
{
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: peerStateRemoved}},
allB: []int64{}},
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}},
},
{
name: "add peer with base > height should error",
fields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
allB: []int64{1, 2, 3, 4}},
args: args{peerID: "P1", base: 6, height: 5},
wantFields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 4, state: peerStateRemoved}}},
wantErr: true,
},
{
name: "add peer with base == height is fine",
fields: scTestParams{
peers: map[string]*scPeer{"P1": {height: 4, state: peerStateNew}},
targetPending: 4,
},
args: args{peerID: "P1", base: 6, height: 6},
wantFields: scTestParams{
targetPending: 4,
peers: map[string]*scPeer{"P1": {base: 6, height: 6, 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)
err := sc.setPeerRange(tt.args.peerID, tt.args.base, tt.args.height)
if (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 TestScGetPeersWithHeight(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: "one Ready higher peer at base",
fields: scTestParams{
targetPending: 4,
peers: map[string]*scPeer{"P1": {base: 4, height: 20, state: peerStateReady}},
allB: []int64{1, 2, 3, 4},
},
args: args{height: 4},
wantResult: []p2p.ID{"P1"},
},
{
name: "one Ready higher peer with higher base",
fields: scTestParams{
targetPending: 4,
peers: map[string]*scPeer{"P1": {base: 10, height: 20, state: peerStateReady}},
allB: []int64{1, 2, 3, 4},
},
args: args{height: 4},
wantResult: []p2p.ID{},
},
{
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)
// getPeersWithHeight should not mutate the scheduler
wantSc := sc
res := sc.getPeersWithHeight(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 above height",
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 an unknown block below base",
fields: scTestParams{
peers: map[string]*scPeer{"P1": {base: 4, height: 6, state: peerStateReady}},
allB: []int64{1, 2, 3, 4, 5, 6}},
args: args{peerID: "P1", height: 3, tm: now},
wantFields: scTestParams{
peers: map[string]*scPeer{"P1": {base: 4, height: 6, state: peerStateReady}},
allB: []int64{1, 2, 3, 4, 5, 6}},
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{
height: 2,
peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
allB: []int64{2},
pending: map[int64]p2p.ID{2: "P1"},
pendingTime: map[int64]time.Time{2: now},
targetPending: 1,
},
args: args{height: 2},
wantFields: scTestParams{
height: 3,
peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
allB: []int64{3},
targetPending: 1,
},
},
{
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)
oldBlockState := sc.getStateAtHeight(tt.args.height)
if err := sc.markProcessed(tt.args.height); (err != nil) != tt.wantErr {
t.Errorf("markProcessed() wantErr %v, error = %v", tt.wantErr, err)
}
if tt.wantErr {
assert.Equal(t, oldBlockState, sc.getStateAtHeight(tt.args.height))
} else {
assert.Equal(t, blockStateProcessed, sc.getStateAtHeight(tt.args.height))
}
wantSc := newTestScheduler(tt.wantFields)
checkSameScheduler(t, wantSc, sc)
})
}
}
func TestScResetState(t *testing.T) {
tests := []struct {
name string
fields scTestParams
state state.State
wantFields scTestParams
}{
{
name: "updates height and initHeight",
fields: scTestParams{
height: 0,
initHeight: 0,
},
state: state.State{LastBlockHeight: 7},
wantFields: scTestParams{
height: 8,
initHeight: 8,
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
sc := newTestScheduler(tt.fields)
e, err := sc.handleResetState(bcResetState{state: tt.state})
require.NoError(t, err)
assert.Equal(t, e, noOp)
wantSc := newTestScheduler(tt.wantFields)
checkSameScheduler(t, wantSc, sc)
})
}
}
func TestScAllBlocksProcessed(t *testing.T) {
now := time.Now()
tests := []struct {
name string
fields scTestParams
wantResult bool
}{
{
name: "no blocks, no peers",
fields: scTestParams{},
wantResult: false,
},
{
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)
checkSameScheduler(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: 11, height: 11},
wantHeight: -1,
},
{
name: "only New blocks",
fields: scTestParams{
initHeight: 3,
peers: map[string]*scPeer{"P1": {height: 6, state: peerStateReady}},
allB: []int64{3, 4, 5, 6},
},
wantHeight: 3,
},
{
name: "only Pending blocks",
fields: scTestParams{
initHeight: 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{
initHeight: 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{
initHeight: 1,
peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}},
allB: []int64{1, 2, 3, 4},
},
wantHeight: 1,
},
{
name: "mixed block states",
fields: scTestParams{
initHeight: 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)
checkSameScheduler(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: "one Ready higher peer with higher base",
fields: scTestParams{
peers: map[string]*scPeer{"P1": {base: 4, height: 6, state: peerStateReady}},
allB: []int64{1, 2, 3, 4, 5, 6},
},
args: args{height: 3},
wantResult: "",
wantError: true,
},
{
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)
checkSameScheduler(t, wantSc, sc)
})
}
}
// makeScBlock makes an empty block.
func makeScBlock(height int64) *types.Block {
return &types.Block{Header: types.Header{Height: height}}
}
// used in place of assert.Equal(t, want, actual) to avoid failures due to
// scheduler.lastAdvanced timestamp inequalities.
func checkSameScheduler(t *testing.T, want *scheduler, actual *scheduler) {
assert.Equal(t, want.initHeight, actual.initHeight)
assert.Equal(t, want.height, actual.height)
assert.Equal(t, want.peers, actual.peers)
assert.Equal(t, want.blockStates, actual.blockStates)
assert.Equal(t, want.pendingBlocks, actual.pendingBlocks)
assert.Equal(t, want.pendingTime, actual.pendingTime)
assert.Equal(t, want.blockStates, actual.blockStates)
assert.Equal(t, want.receivedBlocks, actual.receivedBlocks)
assert.Equal(t, want.blockStates, actual.blockStates)
}
// 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)
}
}
func TestScHandleBlockResponse(t *testing.T) {
now := time.Now()
block6FromP1 := bcBlockResponse{
time: now.Add(time.Millisecond),
peerID: p2p.ID("P1"),
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 TestScHandleNoBlockResponse(t *testing.T) {
now := time.Now()
noBlock6FromP1 := bcNoBlockResponse{
time: now.Add(time.Millisecond),
peerID: p2p.ID("P1"),
height: 6,
}
tests := []struct {
name string
fields scTestParams
wantEvent Event
wantFields scTestParams
wantErr bool
}{
{
name: "empty scheduler",
fields: scTestParams{},
wantEvent: noOpEvent{},
wantFields: scTestParams{},
},
{
name: "noBlock from removed peer",
fields: scTestParams{peers: map[string]*scPeer{"P1": {height: 8, state: peerStateRemoved}}},
wantEvent: noOpEvent{},
wantFields: scTestParams{peers: map[string]*scPeer{"P1": {height: 8, state: peerStateRemoved}}},
},
{
name: "for 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}},
wantEvent: scPeerError{peerID: "P1", reason: fmt.Errorf("some error")},
wantFields: scTestParams{peers: map[string]*scPeer{"P1": {height: 8, state: peerStateRemoved}}},
},
{
name: "noBlock from peer we don't have",
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},
},
wantEvent: noOpEvent{},
wantFields: 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},
},
},
{
name: "noBlock from existing peer",
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},
},
wantEvent: scPeerError{peerID: "P1", reason: fmt.Errorf("some error")},
wantFields: scTestParams{peers: map[string]*scPeer{"P1": {height: 8, state: peerStateRemoved}}},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
sc := newTestScheduler(tt.fields)
event, err := sc.handleNoBlockResponse(noBlock6FromP1)
checkScResults(t, tt.wantErr, err, tt.wantEvent, event)
wantSc := newTestScheduler(tt.wantFields)
assert.Equal(t, wantSc, sc)
})
}
}
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: noOpEvent{},
},
{
name: "processed block we don't have",
fields: scTestParams{
initHeight: 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: noOpEvent{},
},
{
name: "processed block ok, we processed all blocks",
fields: scTestParams{
initHeight: 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: 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: 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: 6,
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: 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: 5,
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: 5,
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 := bcAddNewPeer{
peerID: p2p.ID("P1"),
}
type args struct {
event bcAddNewPeer
}
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{
initHeight: 6,
peers: map[string]*scPeer{"P1": {height: 8, state: peerStateReady}},
allB: []int64{6, 7, 8},
},
args: args{event: addP1},
wantEvent: noOpEvent{},
},
{
name: "add P1 to non empty scheduler",
fields: scTestParams{
initHeight: 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 TestScHandleTryPrunePeer(t *testing.T) {
now := time.Now()
pruneEv := rTryPrunePeer{
time: now.Add(time.Second + time.Millisecond),
}
type args struct {
event rTryPrunePeer
}
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 TestScHandleTrySchedule(t *testing.T) {
now := time.Now()
tryEv := rTrySchedule{
time: now.Add(time.Second + time.Millisecond),
}
type args struct {
event rTrySchedule
}
tests := []struct {
name string
fields scTestParams
args args
wantEvent Event
wantErr bool
}{
{
name: "no peers",
fields: scTestParams{startTime: now, peers: map[string]*scPeer{}},
args: args{event: tryEv},
wantEvent: noOpEvent{},
},
{
name: "only new peers",
fields: scTestParams{startTime: now, peers: map[string]*scPeer{"P1": {height: -1, state: peerStateNew}}},
args: args{event: tryEv},
wantEvent: noOpEvent{},
},
{
name: "only Removed peers",
fields: scTestParams{startTime: now, peers: map[string]*scPeer{"P1": {height: 4, state: peerStateRemoved}}},
args: args{event: tryEv},
wantEvent: noOpEvent{},
},
{
name: "one Ready shorter peer",
fields: scTestParams{
startTime: now,
height: 6,
peers: map[string]*scPeer{"P1": {height: 4, state: peerStateReady}}},
args: args{event: tryEv},
wantEvent: noOpEvent{},
},
{
name: "one Ready equal peer",
fields: scTestParams{
startTime: now,
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{
startTime: now,
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{
startTime: now,
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: noOpEvent{},
},
{
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) {
now := time.Now()
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{startTime: now, peers: map[string]*scPeer{}, height: 1},
args: args{event: bcAddNewPeer{peerID: "P1"}},
wantEvent: noOpEvent{},
wantSc: &scTestParams{startTime: now, peers: map[string]*scPeer{
"P1": {base: -1, 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{
startTime: now,
peers: map[string]*scPeer{"P1": {height: 3, state: peerStateReady}},
allB: []int64{1, 2, 3},
height: 1,
},
},
{ // schedule block 1
args: args{event: rTrySchedule{time: tick[1]}},
wantEvent: scBlockRequest{peerID: "P1", height: 1},
wantSc: &scTestParams{
startTime: now,
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: rTrySchedule{time: tick[2]}},
wantEvent: scBlockRequest{peerID: "P1", height: 2},
wantSc: &scTestParams{
startTime: now,
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: rTrySchedule{time: tick[3]}},
wantEvent: scBlockRequest{peerID: "P1", height: 3},
wantSc: &scTestParams{
startTime: now,
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", time: tick[4], size: 100, block: makeScBlock(1)}},
wantEvent: scBlockReceived{peerID: "P1", block: makeScBlock(1)},
wantSc: &scTestParams{
startTime: now,
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", time: tick[5], size: 100, block: makeScBlock(2)}},
wantEvent: scBlockReceived{peerID: "P1", block: makeScBlock(2)},
wantSc: &scTestParams{
startTime: now,
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", time: tick[6], size: 100, block: makeScBlock(3)}},
wantEvent: scBlockReceived{peerID: "P1", block: makeScBlock(3)},
wantSc: &scTestParams{
startTime: now,
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{
startTime: now,
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{
startTime: now,
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{
startTime: now,
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{
startTime: now,
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,
},
},
},
},
}
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)
wantSc := newTestScheduler(*step.wantSc)
t.Logf("step %d(%v): %s", i, step.args.event, sc)
checkSameScheduler(t, wantSc, sc)
checkScResults(t, step.wantErr, err, step.wantEvent, nextEvent)
// Next step may use the wantedState as their currentState.
sc = newTestScheduler(*step.wantSc)
}
})
}
}