package blocksync import ( "context" "fmt" mrand "math/rand" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tendermint/tendermint/libs/log" tmrand "github.com/tendermint/tendermint/libs/rand" "github.com/tendermint/tendermint/types" ) func init() { peerTimeout = 2 * time.Second } type testPeer struct { id types.NodeID base int64 height int64 inputChan chan inputData // make sure each peer's data is sequential } type inputData struct { t *testing.T pool *BlockPool request BlockRequest } func (p testPeer) runInputRoutine() { go func() { for input := range p.inputChan { p.simulateInput(input) } }() } // Request desired, pretend like we got the block immediately. func (p testPeer) simulateInput(input inputData) { block := &types.Block{Header: types.Header{Height: input.request.Height}} input.pool.AddBlock(input.request.PeerID, block, 123) // TODO: uncommenting this creates a race which is detected by: // https://github.com/golang/go/blob/2bd767b1022dd3254bcec469f0ee164024726486/src/testing/testing.go#L854-L856 // see: https://github.com/tendermint/tendermint/issues/3390#issue-418379890 // input.t.Logf("Added block from peer %v (height: %v)", input.request.PeerID, input.request.Height) } type testPeers map[types.NodeID]testPeer func (ps testPeers) start() { for _, v := range ps { v.runInputRoutine() } } func (ps testPeers) stop() { for _, v := range ps { close(v.inputChan) } } func makePeers(numPeers int, minHeight, maxHeight int64) testPeers { peers := make(testPeers, numPeers) for i := 0; i < numPeers; i++ { peerID := types.NodeID(tmrand.Str(12)) height := minHeight + mrand.Int63n(maxHeight-minHeight) base := minHeight + int64(i) if base > height { base = height } peers[peerID] = testPeer{peerID, base, height, make(chan inputData, 10)} } return peers } func TestBlockPoolBasic(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() start := int64(42) peers := makePeers(10, start+1, 1000) errorsCh := make(chan peerError, 1000) requestsCh := make(chan BlockRequest, 1000) pool := NewBlockPool(log.TestingLogger(), start, requestsCh, errorsCh) if err := pool.Start(ctx); err != nil { t.Error(err) } t.Cleanup(func() { cancel(); pool.Wait() }) peers.start() defer peers.stop() // Introduce each peer. go func() { for _, peer := range peers { pool.SetPeerRange(peer.id, peer.base, peer.height) } }() // Start a goroutine to pull blocks go func() { for { if !pool.IsRunning() { return } first, second := pool.PeekTwoBlocks() if first != nil && second != nil { pool.PopRequest() } else { time.Sleep(1 * time.Second) } } }() // Pull from channels for { select { case err := <-errorsCh: t.Error(err) case request := <-requestsCh: t.Logf("Pulled new BlockRequest %v", request) if request.Height == 300 { return // Done! } peers[request.PeerID].inputChan <- inputData{t, pool, request} } } } func TestBlockPoolTimeout(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() start := int64(42) peers := makePeers(10, start+1, 1000) errorsCh := make(chan peerError, 1000) requestsCh := make(chan BlockRequest, 1000) pool := NewBlockPool(log.TestingLogger(), start, requestsCh, errorsCh) err := pool.Start(ctx) if err != nil { t.Error(err) } t.Cleanup(func() { cancel(); pool.Wait() }) for _, peer := range peers { t.Logf("Peer %v", peer.id) } // Introduce each peer. go func() { for _, peer := range peers { pool.SetPeerRange(peer.id, peer.base, peer.height) } }() // Start a goroutine to pull blocks go func() { for { if !pool.IsRunning() { return } first, second := pool.PeekTwoBlocks() if first != nil && second != nil { pool.PopRequest() } else { time.Sleep(1 * time.Second) } } }() // Pull from channels counter := 0 timedOut := map[types.NodeID]struct{}{} for { select { case err := <-errorsCh: t.Log(err) // consider error to be always timeout here if _, ok := timedOut[err.peerID]; !ok { counter++ if counter == len(peers) { return // Done! } } case request := <-requestsCh: t.Logf("Pulled new BlockRequest %+v", request) } } } func TestBlockPoolRemovePeer(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() peers := make(testPeers, 10) for i := 0; i < 10; i++ { peerID := types.NodeID(fmt.Sprintf("%d", i+1)) height := int64(i + 1) peers[peerID] = testPeer{peerID, 0, height, make(chan inputData)} } requestsCh := make(chan BlockRequest) errorsCh := make(chan peerError) pool := NewBlockPool(log.TestingLogger(), 1, requestsCh, errorsCh) err := pool.Start(ctx) require.NoError(t, err) t.Cleanup(func() { cancel(); pool.Wait() }) // add peers for peerID, peer := range peers { pool.SetPeerRange(peerID, peer.base, peer.height) } assert.EqualValues(t, 10, pool.MaxPeerHeight()) // remove not-existing peer assert.NotPanics(t, func() { pool.RemovePeer(types.NodeID("Superman")) }) // remove peer with biggest height pool.RemovePeer(types.NodeID("10")) assert.EqualValues(t, 9, pool.MaxPeerHeight()) // remove all peers for peerID := range peers { pool.RemovePeer(peerID) } assert.EqualValues(t, 0, pool.MaxPeerHeight()) }