123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270 |
- // 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"
- "testing"
- "github.com/ethereum/go-ethereum/log"
- "github.com/ethereum/go-ethereum/node"
- "github.com/ethereum/go-ethereum/p2p"
- "github.com/ethereum/go-ethereum/p2p/discover"
- "github.com/ethereum/go-ethereum/p2p/simulations"
- "github.com/ethereum/go-ethereum/p2p/simulations/adapters"
- "github.com/ethereum/go-ethereum/rlp"
- "github.com/ethereum/go-ethereum/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(t *testing.T, id discover.NodeID, 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)
- peerIDs := make([]discover.NodeID, n)
- for i := 0; i < n; i++ {
- peers[i] = adapters.RandomNodeConfig()
- peers[i].Services = []string{"mock"}
- peerIDs[i] = peers[i].ID
- }
- events := make(chan *p2p.PeerEvent, 1000)
- node.SubscribeEvents(events)
- ps := &ProtocolSession{
- Server: node.Server(),
- IDs: peerIDs,
- 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 discover.NodeID, 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:
- m.err <- p2p.Send(rw, trig.Code, trig.Msg)
- 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(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(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
- }
|