|
|
@@ -19,13 +19,17 @@ package testing
|
|
|
import (
|
|
|
"errors"
|
|
|
"fmt"
|
|
|
+ "sync"
|
|
|
"time"
|
|
|
|
|
|
+ "github.com/ethereum/go-ethereum/log"
|
|
|
"github.com/ethereum/go-ethereum/p2p"
|
|
|
"github.com/ethereum/go-ethereum/p2p/discover"
|
|
|
"github.com/ethereum/go-ethereum/p2p/simulations/adapters"
|
|
|
)
|
|
|
|
|
|
+var errTimedOut = errors.New("timed out")
|
|
|
+
|
|
|
// ProtocolSession is a quasi simulation of a pivot node running
|
|
|
// a service and a number of dummy peers that can send (trigger) or
|
|
|
// receive (expect) messages
|
|
|
@@ -46,6 +50,7 @@ type Exchange struct {
|
|
|
Label string
|
|
|
Triggers []Trigger
|
|
|
Expects []Expect
|
|
|
+ Timeout time.Duration
|
|
|
}
|
|
|
|
|
|
// Trigger is part of the exchange, incoming message for the pivot node
|
|
|
@@ -102,76 +107,145 @@ func (self *ProtocolSession) trigger(trig Trigger) error {
|
|
|
}
|
|
|
|
|
|
// expect checks an expectation of a message sent out by the pivot node
|
|
|
-func (self *ProtocolSession) expect(exp Expect) error {
|
|
|
- if exp.Msg == nil {
|
|
|
- return errors.New("no message to expect")
|
|
|
- }
|
|
|
- simNode, ok := self.adapter.GetNode(exp.Peer)
|
|
|
- if !ok {
|
|
|
- return fmt.Errorf("trigger: peer %v does not exist (1- %v)", exp.Peer, len(self.IDs))
|
|
|
+func (self *ProtocolSession) expect(exps []Expect) error {
|
|
|
+ // construct a map of expectations for each node
|
|
|
+ peerExpects := make(map[discover.NodeID][]Expect)
|
|
|
+ for _, exp := range exps {
|
|
|
+ if exp.Msg == nil {
|
|
|
+ return errors.New("no message to expect")
|
|
|
+ }
|
|
|
+ peerExpects[exp.Peer] = append(peerExpects[exp.Peer], exp)
|
|
|
}
|
|
|
- mockNode, ok := simNode.Services()[0].(*mockNode)
|
|
|
- if !ok {
|
|
|
- return fmt.Errorf("trigger: peer %v is not a mock", exp.Peer)
|
|
|
+
|
|
|
+ // construct a map of mockNodes for each node
|
|
|
+ mockNodes := make(map[discover.NodeID]*mockNode)
|
|
|
+ for nodeID := range peerExpects {
|
|
|
+ simNode, ok := self.adapter.GetNode(nodeID)
|
|
|
+ if !ok {
|
|
|
+ return fmt.Errorf("trigger: peer %v does not exist (1- %v)", nodeID, len(self.IDs))
|
|
|
+ }
|
|
|
+ mockNode, ok := simNode.Services()[0].(*mockNode)
|
|
|
+ if !ok {
|
|
|
+ return fmt.Errorf("trigger: peer %v is not a mock", nodeID)
|
|
|
+ }
|
|
|
+ mockNodes[nodeID] = mockNode
|
|
|
}
|
|
|
|
|
|
+ // done chanell cancels all created goroutines when function returns
|
|
|
+ done := make(chan struct{})
|
|
|
+ defer close(done)
|
|
|
+ // errc catches the first error from
|
|
|
errc := make(chan error)
|
|
|
+
|
|
|
+ wg := &sync.WaitGroup{}
|
|
|
+ wg.Add(len(mockNodes))
|
|
|
+ for nodeID, mockNode := range mockNodes {
|
|
|
+ nodeID := nodeID
|
|
|
+ mockNode := mockNode
|
|
|
+ go func() {
|
|
|
+ defer wg.Done()
|
|
|
+
|
|
|
+ // Sum all Expect timeouts to give the maximum
|
|
|
+ // time for all expectations to finish.
|
|
|
+ // mockNode.Expect checks all received messages against
|
|
|
+ // a list of expected messages and timeout for each
|
|
|
+ // of them can not be checked separately.
|
|
|
+ var t time.Duration
|
|
|
+ for _, exp := range peerExpects[nodeID] {
|
|
|
+ if exp.Timeout == time.Duration(0) {
|
|
|
+ t += 2000 * time.Millisecond
|
|
|
+ } else {
|
|
|
+ t += exp.Timeout
|
|
|
+ }
|
|
|
+ }
|
|
|
+ alarm := time.NewTimer(t)
|
|
|
+ defer alarm.Stop()
|
|
|
+
|
|
|
+ // expectErrc is used to check if error returned
|
|
|
+ // from mockNode.Expect is not nil and to send it to
|
|
|
+ // errc only in that case.
|
|
|
+ // done channel will be closed when function
|
|
|
+ expectErrc := make(chan error)
|
|
|
+ go func() {
|
|
|
+ select {
|
|
|
+ case expectErrc <- mockNode.Expect(peerExpects[nodeID]...):
|
|
|
+ case <-done:
|
|
|
+ case <-alarm.C:
|
|
|
+ }
|
|
|
+ }()
|
|
|
+
|
|
|
+ select {
|
|
|
+ case err := <-expectErrc:
|
|
|
+ if err != nil {
|
|
|
+ select {
|
|
|
+ case errc <- err:
|
|
|
+ case <-done:
|
|
|
+ case <-alarm.C:
|
|
|
+ errc <- errTimedOut
|
|
|
+ }
|
|
|
+ }
|
|
|
+ case <-done:
|
|
|
+ case <-alarm.C:
|
|
|
+ errc <- errTimedOut
|
|
|
+ }
|
|
|
+
|
|
|
+ }()
|
|
|
+ }
|
|
|
+
|
|
|
go func() {
|
|
|
- errc <- mockNode.Expect(&exp)
|
|
|
+ wg.Wait()
|
|
|
+ // close errc when all goroutines finish to return nill err from errc
|
|
|
+ close(errc)
|
|
|
}()
|
|
|
|
|
|
- t := exp.Timeout
|
|
|
- if t == time.Duration(0) {
|
|
|
- t = 2000 * time.Millisecond
|
|
|
- }
|
|
|
- select {
|
|
|
- case err := <-errc:
|
|
|
- return err
|
|
|
- case <-time.After(t):
|
|
|
- return fmt.Errorf("timout expecting %v sent to peer %v", exp.Msg, exp.Peer)
|
|
|
- }
|
|
|
+ return <-errc
|
|
|
}
|
|
|
|
|
|
// TestExchanges tests a series of exchanges against the session
|
|
|
func (self *ProtocolSession) TestExchanges(exchanges ...Exchange) error {
|
|
|
- // launch all triggers of this exchanges
|
|
|
+ for i, e := range exchanges {
|
|
|
+ if err := self.testExchange(e); err != nil {
|
|
|
+ return fmt.Errorf("exchange #%d %q: %v", i, e.Label, err)
|
|
|
+ }
|
|
|
+ log.Trace(fmt.Sprintf("exchange #%d %q: run successfully", i, e.Label))
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+// testExchange tests a single Exchange.
|
|
|
+// Default timeout value is 2 seconds.
|
|
|
+func (self *ProtocolSession) testExchange(e Exchange) error {
|
|
|
+ errc := make(chan error)
|
|
|
+ done := make(chan struct{})
|
|
|
+ defer close(done)
|
|
|
|
|
|
- for _, e := range exchanges {
|
|
|
- errc := make(chan error, len(e.Triggers)+len(e.Expects))
|
|
|
+ go func() {
|
|
|
for _, trig := range e.Triggers {
|
|
|
- errc <- self.trigger(trig)
|
|
|
+ err := self.trigger(trig)
|
|
|
+ if err != nil {
|
|
|
+ errc <- err
|
|
|
+ return
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- // each expectation is spawned in separate go-routine
|
|
|
- // expectations of an exchange are conjunctive but unordered, i.e.,
|
|
|
- // only all of them arriving constitutes a pass
|
|
|
- // each expectation is meant to be for a different peer, otherwise they are expected to panic
|
|
|
- // testing of an exchange blocks until all expectations are decided
|
|
|
- // an expectation is decided if
|
|
|
- // expected message arrives OR
|
|
|
- // an unexpected message arrives (panic)
|
|
|
- // times out on their individual timeout
|
|
|
- for _, ex := range e.Expects {
|
|
|
- // expect msg spawned to separate go routine
|
|
|
- go func(exp Expect) {
|
|
|
- errc <- self.expect(exp)
|
|
|
- }(ex)
|
|
|
+ select {
|
|
|
+ case errc <- self.expect(e.Expects):
|
|
|
+ case <-done:
|
|
|
}
|
|
|
+ }()
|
|
|
|
|
|
- // time out globally or finish when all expectations satisfied
|
|
|
- timeout := time.After(5 * time.Second)
|
|
|
- for i := 0; i < len(e.Triggers)+len(e.Expects); i++ {
|
|
|
- select {
|
|
|
- case err := <-errc:
|
|
|
- if err != nil {
|
|
|
- return fmt.Errorf("exchange failed with: %v", err)
|
|
|
- }
|
|
|
- case <-timeout:
|
|
|
- return fmt.Errorf("exchange %v: '%v' timed out", i, e.Label)
|
|
|
- }
|
|
|
- }
|
|
|
+ // time out globally or finish when all expectations satisfied
|
|
|
+ t := e.Timeout
|
|
|
+ if t == 0 {
|
|
|
+ t = 2000 * time.Millisecond
|
|
|
+ }
|
|
|
+ alarm := time.NewTimer(t)
|
|
|
+ select {
|
|
|
+ case err := <-errc:
|
|
|
+ return err
|
|
|
+ case <-alarm.C:
|
|
|
+ return errTimedOut
|
|
|
}
|
|
|
- return nil
|
|
|
}
|
|
|
|
|
|
// TestDisconnected tests the disconnections given as arguments
|