aboutsummaryrefslogblamecommitdiffstats
path: root/p2p/testing/protocoltester.go
blob: 4928025870c722439fec7f493bf9b11770a27fa2 (plain) (tree)

























                                                                                  
               
             


                   
              
 







                                                                    











                                                                            
                                                                                                          






















                                                                                  
                                       


                                                      
                                          




                                                 
                               













                                     

                                       




                                                                        
                                                                                  

                                                                
                                                                            

                                                                                      
                                                                


                                                                                      
                                                                          





































                                                                                           
                              







                                             
                                             












                                                                       

                                                              

                                                     










                                                 
                                                







                                               
















                                                                 
                                                                                                             













                                                                                              
                                                                                                                                      






























                                                                                                                                                        













                                           
// Copyright 2017 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.

/*
the p2p/testing package provides a unit test scheme to check simple
protocol message exchanges with one pivot node and a number of dummy peers
The pivot test node runs a node.Service, the dummy peers run a mock node
that can be used to send and receive messages
*/

package testing

import (
    "bytes"
    "fmt"
    "io"
    "io/ioutil"
    "strings"
    "sync"

    "github.com/dexon-foundation/dexon/log"
    "github.com/dexon-foundation/dexon/node"
    "github.com/dexon-foundation/dexon/p2p"
    "github.com/dexon-foundation/dexon/p2p/enode"
    "github.com/dexon-foundation/dexon/p2p/simulations"
    "github.com/dexon-foundation/dexon/p2p/simulations/adapters"
    "github.com/dexon-foundation/dexon/rlp"
    "github.com/dexon-foundation/dexon/rpc"
)

// ProtocolTester is the tester environment used for unit testing protocol
// message exchanges. It uses p2p/simulations framework
type ProtocolTester struct {
    *ProtocolSession
    network *simulations.Network
}

// NewProtocolTester constructs a new ProtocolTester
// it takes as argument the pivot node id, the number of dummy peers and the
// protocol run function called on a peer connection by the p2p server
func NewProtocolTester(id enode.ID, n int, run func(*p2p.Peer, p2p.MsgReadWriter) error) *ProtocolTester {
    services := adapters.Services{
        "test": func(ctx *adapters.ServiceContext) (node.Service, error) {
            return &testNode{run}, nil
        },
        "mock": func(ctx *adapters.ServiceContext) (node.Service, error) {
            return newMockNode(), nil
        },
    }
    adapter := adapters.NewSimAdapter(services)
    net := simulations.NewNetwork(adapter, &simulations.NetworkConfig{})
    if _, err := net.NewNodeWithConfig(&adapters.NodeConfig{
        ID:              id,
        EnableMsgEvents: true,
        Services:        []string{"test"},
    }); err != nil {
        panic(err.Error())
    }
    if err := net.Start(id); err != nil {
        panic(err.Error())
    }

    node := net.GetNode(id).Node.(*adapters.SimNode)
    peers := make([]*adapters.NodeConfig, n)
    nodes := make([]*enode.Node, n)
    for i := 0; i < n; i++ {
        peers[i] = adapters.RandomNodeConfig()
        peers[i].Services = []string{"mock"}
        nodes[i] = peers[i].Node()
    }
    events := make(chan *p2p.PeerEvent, 1000)
    node.SubscribeEvents(events)
    ps := &ProtocolSession{
        Server:  node.Server(),
        Nodes:   nodes,
        adapter: adapter,
        events:  events,
    }
    self := &ProtocolTester{
        ProtocolSession: ps,
        network:         net,
    }

    self.Connect(id, peers...)

    return self
}

// Stop stops the p2p server
func (t *ProtocolTester) Stop() error {
    t.Server.Stop()
    return nil
}

// Connect brings up the remote peer node and connects it using the
// p2p/simulations network connection with the in memory network adapter
func (t *ProtocolTester) Connect(selfID enode.ID, peers ...*adapters.NodeConfig) {
    for _, peer := range peers {
        log.Trace(fmt.Sprintf("start node %v", peer.ID))
        if _, err := t.network.NewNodeWithConfig(peer); err != nil {
            panic(fmt.Sprintf("error starting peer %v: %v", peer.ID, err))
        }
        if err := t.network.Start(peer.ID); err != nil {
            panic(fmt.Sprintf("error starting peer %v: %v", peer.ID, err))
        }
        log.Trace(fmt.Sprintf("connect to %v", peer.ID))
        if err := t.network.Connect(selfID, peer.ID); err != nil {
            panic(fmt.Sprintf("error connecting to peer %v: %v", peer.ID, err))
        }
    }

}

// testNode wraps a protocol run function and implements the node.Service
// interface
type testNode struct {
    run func(*p2p.Peer, p2p.MsgReadWriter) error
}

func (t *testNode) Protocols() []p2p.Protocol {
    return []p2p.Protocol{{
        Length: 100,
        Run:    t.run,
    }}
}

func (t *testNode) APIs() []rpc.API {
    return nil
}

func (t *testNode) Start(server *p2p.Server) error {
    return nil
}

func (t *testNode) Stop() error {
    return nil
}

// mockNode is a testNode which doesn't actually run a protocol, instead
// exposing channels so that tests can manually trigger and expect certain
// messages
type mockNode struct {
    testNode

    trigger  chan *Trigger
    expect   chan []Expect
    err      chan error
    stop     chan struct{}
    stopOnce sync.Once
}

func newMockNode() *mockNode {
    mock := &mockNode{
        trigger: make(chan *Trigger),
        expect:  make(chan []Expect),
        err:     make(chan error),
        stop:    make(chan struct{}),
    }
    mock.testNode.run = mock.Run
    return mock
}

// Run is a protocol run function which just loops waiting for tests to
// instruct it to either trigger or expect a message from the peer
func (m *mockNode) Run(peer *p2p.Peer, rw p2p.MsgReadWriter) error {
    for {
        select {
        case trig := <-m.trigger:
            wmsg := Wrap(trig.Msg)
            m.err <- p2p.Send(rw, trig.Code, wmsg)
        case exps := <-m.expect:
            m.err <- expectMsgs(rw, exps)
        case <-m.stop:
            return nil
        }
    }
}

func (m *mockNode) Trigger(trig *Trigger) error {
    m.trigger <- trig
    return <-m.err
}

func (m *mockNode) Expect(exp ...Expect) error {
    m.expect <- exp
    return <-m.err
}

func (m *mockNode) Stop() error {
    m.stopOnce.Do(func() { close(m.stop) })
    return nil
}

func expectMsgs(rw p2p.MsgReadWriter, exps []Expect) error {
    matched := make([]bool, len(exps))
    for {
        msg, err := rw.ReadMsg()
        if err != nil {
            if err == io.EOF {
                break
            }
            return err
        }
        actualContent, err := ioutil.ReadAll(msg.Payload)
        if err != nil {
            return err
        }
        var found bool
        for i, exp := range exps {
            if exp.Code == msg.Code && bytes.Equal(actualContent, mustEncodeMsg(Wrap(exp.Msg))) {
                if matched[i] {
                    return fmt.Errorf("message #%d received two times", i)
                }
                matched[i] = true
                found = true
                break
            }
        }
        if !found {
            expected := make([]string, 0)
            for i, exp := range exps {
                if matched[i] {
                    continue
                }
                expected = append(expected, fmt.Sprintf("code %d payload %x", exp.Code, mustEncodeMsg(Wrap(exp.Msg))))
            }
            return fmt.Errorf("unexpected message code %d payload %x, expected %s", msg.Code, actualContent, strings.Join(expected, " or "))
        }
        done := true
        for _, m := range matched {
            if !m {
                done = false
                break
            }
        }
        if done {
            return nil
        }
    }
    for i, m := range matched {
        if !m {
            return fmt.Errorf("expected message #%d not received", i)
        }
    }
    return nil
}

// mustEncodeMsg uses rlp to encode a message.
// In case of error it panics.
func mustEncodeMsg(msg interface{}) []byte {
    contentEnc, err := rlp.EncodeToBytes(msg)
    if err != nil {
        panic("content encode error: " + err.Error())
    }
    return contentEnc
}

type WrappedMsg struct {
    Context []byte
    Size    uint32
    Payload []byte
}

func Wrap(msg interface{}) interface{} {
    data, _ := rlp.EncodeToBytes(msg)
    return &WrappedMsg{
        Size:    uint32(len(data)),
        Payload: data,
    }
}