swarm: codebase split from go-ethereum (#1405)

This commit is contained in:
Rafael Matias
2019-06-03 12:28:18 +02:00
committed by Anton Evangelatov
parent 7a22da98b9
commit b046760db1
1540 changed files with 4654 additions and 129393 deletions

View File

@ -0,0 +1,79 @@
// Copyright 2018 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/>.
package simulation
import "github.com/ethereum/go-ethereum/p2p/enode"
// BucketKey is the type that should be used for keys in simulation buckets.
type BucketKey string
// NodeItem returns an item set in ServiceFunc function for a particular node.
func (s *Simulation) NodeItem(id enode.ID, key interface{}) (value interface{}, ok bool) {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.buckets[id]; !ok {
return nil, false
}
return s.buckets[id].Load(key)
}
// SetNodeItem sets a new item associated with the node with provided NodeID.
// Buckets should be used to avoid managing separate simulation global state.
func (s *Simulation) SetNodeItem(id enode.ID, key interface{}, value interface{}) {
s.mu.Lock()
defer s.mu.Unlock()
s.buckets[id].Store(key, value)
}
// NodesItems returns a map of items from all nodes that are all set under the
// same BucketKey.
func (s *Simulation) NodesItems(key interface{}) (values map[enode.ID]interface{}) {
s.mu.RLock()
defer s.mu.RUnlock()
ids := s.NodeIDs()
values = make(map[enode.ID]interface{}, len(ids))
for _, id := range ids {
if _, ok := s.buckets[id]; !ok {
continue
}
if v, ok := s.buckets[id].Load(key); ok {
values[id] = v
}
}
return values
}
// UpNodesItems returns a map of items with the same BucketKey from all nodes that are up.
func (s *Simulation) UpNodesItems(key interface{}) (values map[enode.ID]interface{}) {
s.mu.RLock()
defer s.mu.RUnlock()
ids := s.UpNodeIDs()
values = make(map[enode.ID]interface{})
for _, id := range ids {
if _, ok := s.buckets[id]; !ok {
continue
}
if v, ok := s.buckets[id].Load(key); ok {
values[id] = v
}
}
return values
}

View File

@ -0,0 +1,155 @@
// Copyright 2018 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/>.
package simulation
import (
"sync"
"testing"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/p2p/simulations/adapters"
)
// TestServiceBucket tests all bucket functionality using subtests.
// It constructs a simulation of two nodes by adding items to their buckets
// in ServiceFunc constructor, then by SetNodeItem. Testing UpNodesItems
// is done by stopping one node and validating availability of its items.
func TestServiceBucket(t *testing.T) {
testKey := "Key"
testValue := "Value"
sim := New(map[string]ServiceFunc{
"noop": func(ctx *adapters.ServiceContext, b *sync.Map) (node.Service, func(), error) {
b.Store(testKey, testValue+ctx.Config.ID.String())
return newNoopService(), nil, nil
},
})
defer sim.Close()
id1, err := sim.AddNode()
if err != nil {
t.Fatal(err)
}
id2, err := sim.AddNode()
if err != nil {
t.Fatal(err)
}
t.Run("ServiceFunc bucket Store", func(t *testing.T) {
v, ok := sim.NodeItem(id1, testKey)
if !ok {
t.Fatal("bucket item not found")
}
s, ok := v.(string)
if !ok {
t.Fatal("bucket item value is not string")
}
if s != testValue+id1.String() {
t.Fatalf("expected %q, got %q", testValue+id1.String(), s)
}
v, ok = sim.NodeItem(id2, testKey)
if !ok {
t.Fatal("bucket item not found")
}
s, ok = v.(string)
if !ok {
t.Fatal("bucket item value is not string")
}
if s != testValue+id2.String() {
t.Fatalf("expected %q, got %q", testValue+id2.String(), s)
}
})
customKey := "anotherKey"
customValue := "anotherValue"
t.Run("SetNodeItem", func(t *testing.T) {
sim.SetNodeItem(id1, customKey, customValue)
v, ok := sim.NodeItem(id1, customKey)
if !ok {
t.Fatal("bucket item not found")
}
s, ok := v.(string)
if !ok {
t.Fatal("bucket item value is not string")
}
if s != customValue {
t.Fatalf("expected %q, got %q", customValue, s)
}
_, ok = sim.NodeItem(id2, customKey)
if ok {
t.Fatal("bucket item should not be found")
}
})
if err := sim.StopNode(id2); err != nil {
t.Fatal(err)
}
t.Run("UpNodesItems", func(t *testing.T) {
items := sim.UpNodesItems(testKey)
v, ok := items[id1]
if !ok {
t.Errorf("node 1 item not found")
}
s, ok := v.(string)
if !ok {
t.Fatal("node 1 item value is not string")
}
if s != testValue+id1.String() {
t.Fatalf("expected %q, got %q", testValue+id1.String(), s)
}
_, ok = items[id2]
if ok {
t.Errorf("node 2 item should not be found")
}
})
t.Run("NodeItems", func(t *testing.T) {
items := sim.NodesItems(testKey)
v, ok := items[id1]
if !ok {
t.Errorf("node 1 item not found")
}
s, ok := v.(string)
if !ok {
t.Fatal("node 1 item value is not string")
}
if s != testValue+id1.String() {
t.Fatalf("expected %q, got %q", testValue+id1.String(), s)
}
v, ok = items[id2]
if !ok {
t.Errorf("node 2 item not found")
}
s, ok = v.(string)
if !ok {
t.Fatal("node 1 item value is not string")
}
if s != testValue+id2.String() {
t.Fatalf("expected %q, got %q", testValue+id2.String(), s)
}
})
}

View File

@ -0,0 +1,217 @@
// Copyright 2018 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/>.
package simulation
import (
"context"
"sync"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/ethereum/go-ethereum/p2p/simulations"
)
// PeerEvent is the type of the channel returned by Simulation.PeerEvents.
type PeerEvent struct {
// NodeID is the ID of node that the event is caught on.
NodeID enode.ID
// PeerID is the ID of the peer node that the event is caught on.
PeerID enode.ID
// Event is the event that is caught.
Event *simulations.Event
// Error is the error that may have happened during event watching.
Error error
}
// PeerEventsFilter defines a filter on PeerEvents to exclude messages with
// defined properties. Use PeerEventsFilter methods to set required options.
type PeerEventsFilter struct {
eventType simulations.EventType
connUp *bool
msgReceive *bool
protocol *string
msgCode *uint64
}
// NewPeerEventsFilter returns a new PeerEventsFilter instance.
func NewPeerEventsFilter() *PeerEventsFilter {
return &PeerEventsFilter{}
}
// Connect sets the filter to events when two nodes connect.
func (f *PeerEventsFilter) Connect() *PeerEventsFilter {
f.eventType = simulations.EventTypeConn
b := true
f.connUp = &b
return f
}
// Drop sets the filter to events when two nodes disconnect.
func (f *PeerEventsFilter) Drop() *PeerEventsFilter {
f.eventType = simulations.EventTypeConn
b := false
f.connUp = &b
return f
}
// ReceivedMessages sets the filter to only messages that are received.
func (f *PeerEventsFilter) ReceivedMessages() *PeerEventsFilter {
f.eventType = simulations.EventTypeMsg
b := true
f.msgReceive = &b
return f
}
// SentMessages sets the filter to only messages that are sent.
func (f *PeerEventsFilter) SentMessages() *PeerEventsFilter {
f.eventType = simulations.EventTypeMsg
b := false
f.msgReceive = &b
return f
}
// Protocol sets the filter to only one message protocol.
func (f *PeerEventsFilter) Protocol(p string) *PeerEventsFilter {
f.eventType = simulations.EventTypeMsg
f.protocol = &p
return f
}
// MsgCode sets the filter to only one msg code.
func (f *PeerEventsFilter) MsgCode(c uint64) *PeerEventsFilter {
f.eventType = simulations.EventTypeMsg
f.msgCode = &c
return f
}
// PeerEvents returns a channel of events that are captured by admin peerEvents
// subscription nodes with provided NodeIDs. Additional filters can be set to ignore
// events that are not relevant.
func (s *Simulation) PeerEvents(ctx context.Context, ids []enode.ID, filters ...*PeerEventsFilter) <-chan PeerEvent {
eventC := make(chan PeerEvent)
// wait group to make sure all subscriptions to admin peerEvents are established
// before this function returns.
var subsWG sync.WaitGroup
for _, id := range ids {
s.shutdownWG.Add(1)
subsWG.Add(1)
go func(id enode.ID) {
defer s.shutdownWG.Done()
events := make(chan *simulations.Event)
sub := s.Net.Events().Subscribe(events)
defer sub.Unsubscribe()
subsWG.Done()
for {
select {
case <-ctx.Done():
if err := ctx.Err(); err != nil {
select {
case eventC <- PeerEvent{NodeID: id, Error: err}:
case <-s.Done():
}
}
return
case <-s.Done():
return
case e := <-events:
// ignore control events
if e.Control {
continue
}
match := len(filters) == 0 // if there are no filters match all events
for _, f := range filters {
if f.eventType == simulations.EventTypeConn && e.Conn != nil {
if *f.connUp != e.Conn.Up {
continue
}
// all connection filter parameters matched, break the loop
match = true
break
}
if f.eventType == simulations.EventTypeMsg && e.Msg != nil {
if f.msgReceive != nil && *f.msgReceive != e.Msg.Received {
continue
}
if f.protocol != nil && *f.protocol != e.Msg.Protocol {
continue
}
if f.msgCode != nil && *f.msgCode != e.Msg.Code {
continue
}
// all message filter parameters matched, break the loop
match = true
break
}
}
var peerID enode.ID
switch e.Type {
case simulations.EventTypeConn:
peerID = e.Conn.One
if peerID == id {
peerID = e.Conn.Other
}
case simulations.EventTypeMsg:
peerID = e.Msg.One
if peerID == id {
peerID = e.Msg.Other
}
}
if match {
select {
case eventC <- PeerEvent{NodeID: id, PeerID: peerID, Event: e}:
case <-ctx.Done():
if err := ctx.Err(); err != nil {
select {
case eventC <- PeerEvent{NodeID: id, PeerID: peerID, Error: err}:
case <-s.Done():
}
}
return
case <-s.Done():
return
}
}
case err := <-sub.Err():
if err != nil {
select {
case eventC <- PeerEvent{NodeID: id, Error: err}:
case <-ctx.Done():
if err := ctx.Err(); err != nil {
select {
case eventC <- PeerEvent{NodeID: id, Error: err}:
case <-s.Done():
}
}
return
case <-s.Done():
return
}
}
}
}
}(id)
}
// wait all subscriptions
subsWG.Wait()
return eventC
}

View File

@ -0,0 +1,107 @@
// Copyright 2018 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/>.
package simulation
import (
"context"
"sync"
"testing"
"time"
)
// TestPeerEvents creates simulation, adds two nodes,
// register for peer events, connects nodes in a chain
// and waits for the number of connection events to
// be received.
func TestPeerEvents(t *testing.T) {
sim := New(noopServiceFuncMap)
defer sim.Close()
_, err := sim.AddNodes(2)
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
events := sim.PeerEvents(ctx, sim.NodeIDs())
// two nodes -> two connection events
expectedEventCount := 2
var wg sync.WaitGroup
wg.Add(expectedEventCount)
go func() {
for e := range events {
if e.Error != nil {
if e.Error == context.Canceled {
return
}
t.Error(e.Error)
continue
}
wg.Done()
}
}()
err = sim.Net.ConnectNodesChain(sim.NodeIDs())
if err != nil {
t.Fatal(err)
}
wg.Wait()
}
func TestPeerEventsTimeout(t *testing.T) {
sim := New(noopServiceFuncMap)
defer sim.Close()
_, err := sim.AddNodes(2)
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
events := sim.PeerEvents(ctx, sim.NodeIDs())
done := make(chan struct{})
errC := make(chan error)
go func() {
for e := range events {
if e.Error == context.Canceled {
return
}
if e.Error == context.DeadlineExceeded {
close(done)
return
} else {
errC <- e.Error
}
}
}()
select {
case <-time.After(time.Second):
t.Fatal("no context deadline received")
case err := <-errC:
t.Fatal(err)
case <-done:
// all good, context deadline detected
}
}

View File

@ -0,0 +1,141 @@
// Copyright 2018 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/>.
package simulation_test
import (
"context"
"fmt"
"sync"
"time"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/p2p/simulations/adapters"
"github.com/ethersphere/swarm/network"
"github.com/ethersphere/swarm/network/simulation"
)
// Every node can have a Kademlia associated using the node bucket under
// BucketKeyKademlia key. This allows to use WaitTillHealthy to block until
// all nodes have the their Kademlias healthy.
func ExampleSimulation_WaitTillHealthy() {
sim := simulation.New(map[string]simulation.ServiceFunc{
"bzz": func(ctx *adapters.ServiceContext, b *sync.Map) (node.Service, func(), error) {
addr := network.NewAddr(ctx.Config.Node())
hp := network.NewHiveParams()
hp.Discovery = false
config := &network.BzzConfig{
OverlayAddr: addr.Over(),
UnderlayAddr: addr.Under(),
HiveParams: hp,
}
kad := network.NewKademlia(addr.Over(), network.NewKadParams())
// store kademlia in node's bucket under BucketKeyKademlia
// so that it can be found by WaitTillHealthy method.
b.Store(simulation.BucketKeyKademlia, kad)
return network.NewBzz(config, kad, nil, nil, nil), nil, nil
},
})
defer sim.Close()
_, err := sim.AddNodesAndConnectRing(10)
if err != nil {
// handle error properly...
panic(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
ill, err := sim.WaitTillHealthy(ctx)
if err != nil {
// inspect the latest detected not healthy kademlias
for id, kad := range ill {
fmt.Println("Node", id)
fmt.Println(kad.String())
}
// handle error...
}
// continue with the test
}
// Watch all peer events in the simulation network, buy receiving from a channel.
func ExampleSimulation_PeerEvents() {
sim := simulation.New(nil)
defer sim.Close()
events := sim.PeerEvents(context.Background(), sim.NodeIDs())
go func() {
for e := range events {
if e.Error != nil {
log.Error("peer event", "err", e.Error)
continue
}
log.Info("peer event", "node", e.NodeID, "peer", e.PeerID, "type", e.Event.Type)
}
}()
}
// Detect when a nodes drop a peer.
func ExampleSimulation_PeerEvents_disconnections() {
sim := simulation.New(nil)
defer sim.Close()
disconnections := sim.PeerEvents(
context.Background(),
sim.NodeIDs(),
simulation.NewPeerEventsFilter().Drop(),
)
go func() {
for d := range disconnections {
if d.Error != nil {
log.Error("peer drop", "err", d.Error)
continue
}
log.Warn("peer drop", "node", d.NodeID, "peer", d.PeerID)
}
}()
}
// Watch multiple types of events or messages. In this case, they differ only
// by MsgCode, but filters can be set for different types or protocols, too.
func ExampleSimulation_PeerEvents_multipleFilters() {
sim := simulation.New(nil)
defer sim.Close()
msgs := sim.PeerEvents(
context.Background(),
sim.NodeIDs(),
// Watch when bzz messages 1 and 4 are received.
simulation.NewPeerEventsFilter().ReceivedMessages().Protocol("bzz").MsgCode(1),
simulation.NewPeerEventsFilter().ReceivedMessages().Protocol("bzz").MsgCode(4),
)
go func() {
for m := range msgs {
if m.Error != nil {
log.Error("bzz message", "err", m.Error)
continue
}
log.Info("bzz message", "node", m.NodeID, "peer", m.PeerID)
}
}()
}

View File

@ -0,0 +1,68 @@
// Copyright 2018 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/>.
package simulation
import (
"fmt"
"net/http"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p/simulations"
)
// Package defaults.
var (
DefaultHTTPSimAddr = ":8888"
)
//WithServer implements the builder pattern constructor for Simulation to
//start with a HTTP server
func (s *Simulation) WithServer(addr string) *Simulation {
//assign default addr if nothing provided
if addr == "" {
addr = DefaultHTTPSimAddr
}
log.Info(fmt.Sprintf("Initializing simulation server on %s...", addr))
//initialize the HTTP server
s.handler = simulations.NewServer(s.Net)
s.runC = make(chan struct{})
//add swarm specific routes to the HTTP server
s.addSimulationRoutes()
s.httpSrv = &http.Server{
Addr: addr,
Handler: s.handler,
}
go func() {
err := s.httpSrv.ListenAndServe()
if err != nil {
log.Error("Error starting the HTTP server", "error", err)
}
}()
return s
}
//register additional HTTP routes
func (s *Simulation) addSimulationRoutes() {
s.handler.POST("/runsim", s.RunSimulation)
}
// RunSimulation is the actual POST endpoint runner
func (s *Simulation) RunSimulation(w http.ResponseWriter, req *http.Request) {
log.Debug("RunSimulation endpoint running")
s.runC <- struct{}{}
w.WriteHeader(http.StatusOK)
}

View File

@ -0,0 +1,110 @@
// Copyright 2018 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/>.
package simulation
import (
"context"
"fmt"
"net/http"
"sync"
"testing"
"time"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/p2p/simulations/adapters"
)
func TestSimulationWithHTTPServer(t *testing.T) {
log.Debug("Init simulation")
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
sim := New(
map[string]ServiceFunc{
"noop": func(_ *adapters.ServiceContext, b *sync.Map) (node.Service, func(), error) {
return newNoopService(), nil, nil
},
}).WithServer(DefaultHTTPSimAddr)
defer sim.Close()
log.Debug("Done.")
_, err := sim.AddNode()
if err != nil {
t.Fatal(err)
}
log.Debug("Starting sim round and let it time out...")
//first test that running without sending to the channel will actually
//block the simulation, so let it time out
result := sim.Run(ctx, func(ctx context.Context, sim *Simulation) error {
log.Debug("Just start the sim without any action and wait for the timeout")
//ensure with a Sleep that simulation doesn't terminate before the timeout
time.Sleep(2 * time.Second)
return nil
})
if result.Error != nil {
if result.Error.Error() == "context deadline exceeded" {
log.Debug("Expected timeout error received")
} else {
t.Fatal(result.Error)
}
}
//now run it again and send the expected signal on the waiting channel,
//then close the simulation
log.Debug("Starting sim round and wait for frontend signal...")
//this time the timeout should be long enough so that it doesn't kick in too early
ctx, cancel2 := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel2()
errC := make(chan error, 1)
go triggerSimulationRun(t, errC)
result = sim.Run(ctx, func(ctx context.Context, sim *Simulation) error {
log.Debug("This run waits for the run signal from `frontend`...")
//ensure with a Sleep that simulation doesn't terminate before the signal is received
time.Sleep(2 * time.Second)
return nil
})
if result.Error != nil {
t.Fatal(result.Error)
}
if err := <-errC; err != nil {
t.Fatal(err)
}
log.Debug("Test terminated successfully")
}
func triggerSimulationRun(t *testing.T, errC chan error) {
//We need to first wait for the sim HTTP server to start running...
time.Sleep(2 * time.Second)
//then we can send the signal
log.Debug("Sending run signal to simulation: POST /runsim...")
resp, err := http.Post(fmt.Sprintf("http://localhost%s/runsim", DefaultHTTPSimAddr), "application/json", nil)
if err != nil {
errC <- fmt.Errorf("Request failed: %v", err)
return
}
log.Debug("Signal sent")
if resp.StatusCode != http.StatusOK {
errC <- fmt.Errorf("err %s", resp.Status)
return
}
errC <- resp.Body.Close()
}

View File

@ -0,0 +1,203 @@
// Copyright 2018 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/>.
package simulation
import (
"context"
"encoding/binary"
"encoding/hex"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/ethereum/go-ethereum/p2p/simulations"
"github.com/ethersphere/swarm/network"
)
// BucketKeyKademlia is the key to be used for storing the kademlia
// instance for particular node, usually inside the ServiceFunc function.
var BucketKeyKademlia BucketKey = "kademlia"
// WaitTillHealthy is blocking until the health of all kademlias is true.
// If error is not nil, a map of kademlia that was found not healthy is returned.
// TODO: Check correctness since change in kademlia depth calculation logic
func (s *Simulation) WaitTillHealthy(ctx context.Context) (ill map[enode.ID]*network.Kademlia, err error) {
// Prepare PeerPot map for checking Kademlia health
var ppmap map[string]*network.PeerPot
kademlias := s.kademlias()
addrs := make([][]byte, 0, len(kademlias))
// TODO verify that all kademlias have same params
for _, k := range kademlias {
addrs = append(addrs, k.BaseAddr())
}
ppmap = network.NewPeerPotMap(s.neighbourhoodSize, addrs)
// Wait for healthy Kademlia on every node before checking files
ticker := time.NewTicker(200 * time.Millisecond)
defer ticker.Stop()
ill = make(map[enode.ID]*network.Kademlia)
for {
select {
case <-ctx.Done():
return ill, ctx.Err()
case <-ticker.C:
for k := range ill {
delete(ill, k)
}
log.Debug("kademlia health check", "addr count", len(addrs), "kad len", len(kademlias))
for id, k := range kademlias {
//PeerPot for this node
addr := common.Bytes2Hex(k.BaseAddr())
pp := ppmap[addr]
//call Healthy RPC
h := k.GetHealthInfo(pp)
//print info
log.Debug(k.String())
log.Debug("kademlia", "connectNN", h.ConnectNN, "knowNN", h.KnowNN)
log.Debug("kademlia", "health", h.ConnectNN && h.KnowNN, "addr", hex.EncodeToString(k.BaseAddr()), "node", id)
log.Debug("kademlia", "ill condition", !h.ConnectNN, "addr", hex.EncodeToString(k.BaseAddr()), "node", id)
if !h.Healthy() {
ill[id] = k
}
}
if len(ill) == 0 {
return nil, nil
}
}
}
}
// kademlias returns all Kademlia instances that are set
// in simulation bucket.
func (s *Simulation) kademlias() (ks map[enode.ID]*network.Kademlia) {
items := s.UpNodesItems(BucketKeyKademlia)
log.Debug("kademlia len items", "len", len(items))
ks = make(map[enode.ID]*network.Kademlia, len(items))
for id, v := range items {
k, ok := v.(*network.Kademlia)
if !ok {
continue
}
ks[id] = k
}
return ks
}
// WaitTillSnapshotRecreated is blocking until all the connections specified
// in the snapshot are registered in the kademlia.
// It differs from WaitTillHealthy, which waits only until all the kademlias are
// healthy (it might happen even before all the connections are established).
func (s *Simulation) WaitTillSnapshotRecreated(ctx context.Context, snap *simulations.Snapshot) error {
expected := getSnapshotConnections(snap.Conns)
ticker := time.NewTicker(150 * time.Millisecond)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-ticker.C:
actual := s.getActualConnections()
if isAllDeployed(expected, actual) {
return nil
}
}
}
}
func (s *Simulation) getActualConnections() (res []uint64) {
kademlias := s.kademlias()
for base, k := range kademlias {
k.EachConn(base[:], 256, func(p *network.Peer, _ int) bool {
res = append(res, getConnectionHash(base, p.ID()))
return true
})
}
// only list those connections that appear twice (both peers should recognize connection as active)
res = removeDuplicatesAndSingletons(res)
return res
}
func getSnapshotConnections(conns []simulations.Conn) (res []uint64) {
for _, c := range conns {
res = append(res, getConnectionHash(c.One, c.Other))
}
return res
}
// returns an integer connection identifier (similar to 8-byte hash)
func getConnectionHash(a, b enode.ID) uint64 {
var h [8]byte
for i := 0; i < 8; i++ {
h[i] = a[i] ^ b[i]
}
res := binary.LittleEndian.Uint64(h[:])
return res
}
// returns true if all connections in expected are listed in actual
func isAllDeployed(expected []uint64, actual []uint64) bool {
if len(expected) == 0 {
return true
}
exp := make([]uint64, len(expected))
copy(exp, expected)
for _, c := range actual {
// remove value c from exp
for i := 0; i < len(exp); i++ {
if exp[i] == c {
exp = removeListElement(exp, i)
if len(exp) == 0 {
return true
}
}
}
}
return len(exp) == 0
}
func removeListElement(arr []uint64, i int) []uint64 {
last := len(arr) - 1
arr[i] = arr[last]
arr = arr[:last]
return arr
}
func removeDuplicatesAndSingletons(arr []uint64) []uint64 {
for i := 0; i < len(arr); {
found := false
for j := i + 1; j < len(arr); j++ {
if arr[i] == arr[j] {
arr = removeListElement(arr, j) // remove duplicate
found = true
break
}
}
if found {
i++
} else {
arr = removeListElement(arr, i) // remove singleton
}
}
return arr
}

View File

@ -0,0 +1,310 @@
// Copyright 2018 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/>.
package simulation
import (
"context"
"sync"
"testing"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/p2p/simulations/adapters"
"github.com/ethersphere/swarm/network"
)
/*
TestWaitTillHealthy tests that we indeed get a healthy network after we wait for it.
For this to be tested, a bit of a snake tail bite needs to happen:
* First we create a first simulation
* Run it as nodes connected in a ring
* Wait until the network is healthy
* Then we create a snapshot
* With this snapshot we create a new simulation
* This simulation is expected to have a healthy configuration, as it uses the snapshot
* Thus we just iterate all nodes and check that their kademlias are healthy
* If all kademlias are healthy, the test succeeded, otherwise it failed
*/
func TestWaitTillHealthy(t *testing.T) {
t.Skip("this test is flaky; disabling till underlying problem is solved")
testNodesNum := 10
// create the first simulation
sim := New(createSimServiceMap(true))
// connect and...
nodeIDs, err := sim.AddNodesAndConnectRing(testNodesNum)
if err != nil {
t.Fatal(err)
}
// array of all overlay addresses
var addrs [][]byte
// iterate once to be able to build the peer map
for _, node := range nodeIDs {
//get the kademlia overlay address from this ID
a := node.Bytes()
//append it to the array of all overlay addresses
addrs = append(addrs, a)
}
// build a PeerPot only once
pp := network.NewPeerPotMap(network.NewKadParams().NeighbourhoodSize, addrs)
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
defer cancel()
// ...wait until healthy
ill, err := sim.WaitTillHealthy(ctx)
if err != nil {
for id, kad := range ill {
t.Log("Node", id)
t.Log(kad.String())
}
t.Fatal(err)
}
// now create a snapshot of this network
snap, err := sim.Net.Snapshot()
if err != nil {
t.Fatal(err)
}
// close the initial simulation
sim.Close()
// create a control simulation
controlSim := New(createSimServiceMap(false))
defer controlSim.Close()
// load the snapshot into this control simulation
err = controlSim.Net.Load(snap)
if err != nil {
t.Fatal(err)
}
_, err = controlSim.WaitTillHealthy(ctx)
if err != nil {
t.Fatal(err)
}
for _, node := range nodeIDs {
// ...get its kademlia
item, ok := controlSim.NodeItem(node, BucketKeyKademlia)
if !ok {
t.Fatal("No kademlia bucket item")
}
kad := item.(*network.Kademlia)
// get its base address
kid := common.Bytes2Hex(kad.BaseAddr())
//get the health info
info := kad.GetHealthInfo(pp[kid])
log.Trace("Health info", "info", info)
// check that it is healthy
healthy := info.Healthy()
if !healthy {
t.Fatalf("Expected node %v of control simulation to be healthy, but it is not, unhealthy kademlias: %v", node, kad.String())
}
}
}
// createSimServiceMap returns the services map
// this function will create the sim services with or without discovery enabled
// based on the flag passed
func createSimServiceMap(discovery bool) map[string]ServiceFunc {
return map[string]ServiceFunc{
"bzz": func(ctx *adapters.ServiceContext, b *sync.Map) (node.Service, func(), error) {
addr := network.NewAddr(ctx.Config.Node())
hp := network.NewHiveParams()
hp.Discovery = discovery
config := &network.BzzConfig{
OverlayAddr: addr.Over(),
UnderlayAddr: addr.Under(),
HiveParams: hp,
}
kad := network.NewKademlia(addr.Over(), network.NewKadParams())
// store kademlia in node's bucket under BucketKeyKademlia
// so that it can be found by WaitTillHealthy method.
b.Store(BucketKeyKademlia, kad)
return network.NewBzz(config, kad, nil, nil, nil), nil, nil
},
}
}
// TestWaitTillSnapshotRecreated tests that we indeed have a network
// configuration specified in the snapshot file, after we wait for it.
//
// First we create a first simulation
// Run it as nodes connected in a ring
// Wait until the network is healthy
// Then we create a snapshot
// With this snapshot we create a new simulation
// Call WaitTillSnapshotRecreated() function and wait until it returns
// Iterate the nodes and check if all the connections are successfully recreated
func TestWaitTillSnapshotRecreated(t *testing.T) {
t.Skip("test is flaky. disabling until underlying problem is addressed")
var err error
sim := New(createSimServiceMap(true))
_, err = sim.AddNodesAndConnectRing(16)
if err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
_, err = sim.WaitTillHealthy(ctx)
if err != nil {
t.Fatal(err)
}
originalConnections := sim.getActualConnections()
snap, err := sim.Net.Snapshot()
sim.Close()
if err != nil {
t.Fatal(err)
}
controlSim := New(createSimServiceMap(false))
defer controlSim.Close()
err = controlSim.Net.Load(snap)
if err != nil {
t.Fatal(err)
}
err = controlSim.WaitTillSnapshotRecreated(ctx, snap)
if err != nil {
t.Fatal(err)
}
controlConnections := controlSim.getActualConnections()
for _, c := range originalConnections {
if !exist(controlConnections, c) {
t.Fatal("connection was not recreated")
}
}
}
// exist returns true if val is found in arr
func exist(arr []uint64, val uint64) bool {
for _, c := range arr {
if c == val {
return true
}
}
return false
}
func TestRemoveDuplicatesAndSingletons(t *testing.T) {
singletons := []uint64{
0x3c127c6f6cb026b0,
0x0f45190d72e71fc5,
0xb0184c02449e0bb6,
0xa85c7b84239c54d3,
0xe3b0c44298fc1c14,
0x9afbf4c8996fb924,
0x27ae41e4649b934c,
0xa495991b7852b855,
}
doubles := []uint64{
0x1b879f878de7fc7a,
0xc6791470521bdab4,
0xdd34b0ee39bbccc6,
0x4d904fbf0f31da10,
0x6403c2560432c8f8,
0x18954e33cf3ad847,
0x90db00e98dc7a8a6,
0x92886b0dfcc1809b,
}
var arr []uint64
arr = append(arr, doubles...)
arr = append(arr, singletons...)
arr = append(arr, doubles...)
arr = removeDuplicatesAndSingletons(arr)
for _, i := range singletons {
if exist(arr, i) {
t.Fatalf("singleton not removed: %d", i)
}
}
for _, i := range doubles {
if !exist(arr, i) {
t.Fatalf("wrong value removed: %d", i)
}
}
for j := 0; j < len(doubles); j++ {
v := doubles[j] + singletons[j]
if exist(arr, v) {
t.Fatalf("non-existing value found, index: %d", j)
}
}
}
func TestIsAllDeployed(t *testing.T) {
a := []uint64{
0x3c127c6f6cb026b0,
0x0f45190d72e71fc5,
0xb0184c02449e0bb6,
0xa85c7b84239c54d3,
0xe3b0c44298fc1c14,
0x9afbf4c8996fb924,
0x27ae41e4649b934c,
0xa495991b7852b855,
}
b := []uint64{
0x1b879f878de7fc7a,
0xc6791470521bdab4,
0xdd34b0ee39bbccc6,
0x4d904fbf0f31da10,
0x6403c2560432c8f8,
0x18954e33cf3ad847,
0x90db00e98dc7a8a6,
0x92886b0dfcc1809b,
}
var c []uint64
c = append(c, a...)
c = append(c, b...)
if !isAllDeployed(a, c) {
t.Fatal("isAllDeployed failed")
}
if !isAllDeployed(b, c) {
t.Fatal("isAllDeployed failed")
}
if isAllDeployed(c, a) {
t.Fatal("isAllDeployed failed: false positive")
}
if isAllDeployed(c, b) {
t.Fatal("isAllDeployed failed: false positive")
}
c = c[2:]
if isAllDeployed(a, c) {
t.Fatal("isAllDeployed failed: false positive")
}
if !isAllDeployed(b, c) {
t.Fatal("isAllDeployed failed")
}
}

341
network/simulation/node.go Normal file
View File

@ -0,0 +1,341 @@
// Copyright 2018 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/>.
package simulation
import (
"bytes"
"context"
"crypto/ecdsa"
"encoding/json"
"errors"
"io/ioutil"
"math/rand"
"os"
"sync"
"time"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/ethereum/go-ethereum/p2p/simulations"
"github.com/ethereum/go-ethereum/p2p/simulations/adapters"
"github.com/ethersphere/swarm/network"
)
var (
BucketKeyBzzPrivateKey BucketKey = "bzzprivkey"
)
// NodeIDs returns NodeIDs for all nodes in the network.
func (s *Simulation) NodeIDs() (ids []enode.ID) {
nodes := s.Net.GetNodes()
ids = make([]enode.ID, len(nodes))
for i, node := range nodes {
ids[i] = node.ID()
}
return ids
}
// UpNodeIDs returns NodeIDs for nodes that are up in the network.
func (s *Simulation) UpNodeIDs() (ids []enode.ID) {
nodes := s.Net.GetNodes()
for _, node := range nodes {
if node.Up() {
ids = append(ids, node.ID())
}
}
return ids
}
// DownNodeIDs returns NodeIDs for nodes that are stopped in the network.
func (s *Simulation) DownNodeIDs() (ids []enode.ID) {
nodes := s.Net.GetNodes()
for _, node := range nodes {
if !node.Up() {
ids = append(ids, node.ID())
}
}
return ids
}
// AddNodeOption defines the option that can be passed
// to Simulation.AddNode method.
type AddNodeOption func(*adapters.NodeConfig)
// AddNodeWithMsgEvents sets the EnableMsgEvents option
// to NodeConfig.
func AddNodeWithMsgEvents(enable bool) AddNodeOption {
return func(o *adapters.NodeConfig) {
o.EnableMsgEvents = enable
}
}
// AddNodeWithService specifies a service that should be
// started on a node. This option can be repeated as variadic
// argument toe AddNode and other add node related methods.
// If AddNodeWithService is not specified, all services will be started.
func AddNodeWithService(serviceName string) AddNodeOption {
return func(o *adapters.NodeConfig) {
o.Services = append(o.Services, serviceName)
}
}
// AddNode creates a new node with random configuration,
// applies provided options to the config and adds the node to network.
// By default all services will be started on a node. If one or more
// AddNodeWithService option are provided, only specified services will be started.
func (s *Simulation) AddNode(opts ...AddNodeOption) (id enode.ID, err error) {
conf := adapters.RandomNodeConfig()
for _, o := range opts {
o(conf)
}
if len(conf.Services) == 0 {
conf.Services = s.serviceNames
}
// add ENR records to the underlying node
// most importantly the bzz overlay address
//
// for now we have no way of setting bootnodes or lightnodes in sims
// so we just let them be set to false
// they should perhaps be possible to override them with AddNodeOption
bzzPrivateKey, err := BzzPrivateKeyFromConfig(conf)
if err != nil {
return enode.ID{}, err
}
enodeParams := &network.EnodeParams{
PrivateKey: bzzPrivateKey,
}
record, err := network.NewEnodeRecord(enodeParams)
conf.Record = *record
// Add the bzz address to the node config
node, err := s.Net.NewNodeWithConfig(conf)
if err != nil {
return id, err
}
s.buckets[node.ID()] = new(sync.Map)
s.SetNodeItem(node.ID(), BucketKeyBzzPrivateKey, bzzPrivateKey)
return node.ID(), s.Net.Start(node.ID())
}
// AddNodes creates new nodes with random configurations,
// applies provided options to the config and adds nodes to network.
func (s *Simulation) AddNodes(count int, opts ...AddNodeOption) (ids []enode.ID, err error) {
ids = make([]enode.ID, 0, count)
for i := 0; i < count; i++ {
id, err := s.AddNode(opts...)
if err != nil {
return nil, err
}
ids = append(ids, id)
}
return ids, nil
}
// AddNodesAndConnectFull is a helpper method that combines
// AddNodes and ConnectNodesFull. Only new nodes will be connected.
func (s *Simulation) AddNodesAndConnectFull(count int, opts ...AddNodeOption) (ids []enode.ID, err error) {
if count < 2 {
return nil, errors.New("count of nodes must be at least 2")
}
ids, err = s.AddNodes(count, opts...)
if err != nil {
return nil, err
}
err = s.Net.ConnectNodesFull(ids)
if err != nil {
return nil, err
}
return ids, nil
}
// AddNodesAndConnectChain is a helpper method that combines
// AddNodes and ConnectNodesChain. The chain will be continued from the last
// added node, if there is one in simulation using ConnectToLastNode method.
func (s *Simulation) AddNodesAndConnectChain(count int, opts ...AddNodeOption) (ids []enode.ID, err error) {
if count < 2 {
return nil, errors.New("count of nodes must be at least 2")
}
id, err := s.AddNode(opts...)
if err != nil {
return nil, err
}
err = s.Net.ConnectToLastNode(id)
if err != nil {
return nil, err
}
ids, err = s.AddNodes(count-1, opts...)
if err != nil {
return nil, err
}
ids = append([]enode.ID{id}, ids...)
err = s.Net.ConnectNodesChain(ids)
if err != nil {
return nil, err
}
return ids, nil
}
// AddNodesAndConnectRing is a helpper method that combines
// AddNodes and ConnectNodesRing.
func (s *Simulation) AddNodesAndConnectRing(count int, opts ...AddNodeOption) (ids []enode.ID, err error) {
if count < 2 {
return nil, errors.New("count of nodes must be at least 2")
}
ids, err = s.AddNodes(count, opts...)
if err != nil {
return nil, err
}
err = s.Net.ConnectNodesRing(ids)
if err != nil {
return nil, err
}
return ids, nil
}
// AddNodesAndConnectStar is a helpper method that combines
// AddNodes and ConnectNodesStar.
func (s *Simulation) AddNodesAndConnectStar(count int, opts ...AddNodeOption) (ids []enode.ID, err error) {
if count < 2 {
return nil, errors.New("count of nodes must be at least 2")
}
ids, err = s.AddNodes(count, opts...)
if err != nil {
return nil, err
}
err = s.Net.ConnectNodesStar(ids[1:], ids[0])
if err != nil {
return nil, err
}
return ids, nil
}
// UploadSnapshot uploads a snapshot to the simulation
// This method tries to open the json file provided, applies the config to all nodes
// and then loads the snapshot into the Simulation network
func (s *Simulation) UploadSnapshot(ctx context.Context, snapshotFile string, opts ...AddNodeOption) error {
f, err := os.Open(snapshotFile)
if err != nil {
return err
}
jsonbyte, err := ioutil.ReadAll(f)
f.Close()
if err != nil {
return err
}
var snap simulations.Snapshot
if err := json.Unmarshal(jsonbyte, &snap); err != nil {
return err
}
//the snapshot probably has the property EnableMsgEvents not set
//set it to true (we need this to wait for messages before uploading)
for i := range snap.Nodes {
snap.Nodes[i].Node.Config.EnableMsgEvents = true
snap.Nodes[i].Node.Config.Services = s.serviceNames
for _, o := range opts {
o(snap.Nodes[i].Node.Config)
}
}
if err := s.Net.Load(&snap); err != nil {
return err
}
return s.WaitTillSnapshotRecreated(ctx, &snap)
}
// StartNode starts a node by NodeID.
func (s *Simulation) StartNode(id enode.ID) (err error) {
return s.Net.Start(id)
}
// StartRandomNode starts a random node.
func (s *Simulation) StartRandomNode() (id enode.ID, err error) {
n := s.Net.GetRandomDownNode()
if n == nil {
return id, ErrNodeNotFound
}
return n.ID(), s.Net.Start(n.ID())
}
// StartRandomNodes starts random nodes.
func (s *Simulation) StartRandomNodes(count int) (ids []enode.ID, err error) {
ids = make([]enode.ID, 0, count)
for i := 0; i < count; i++ {
n := s.Net.GetRandomDownNode()
if n == nil {
return nil, ErrNodeNotFound
}
err = s.Net.Start(n.ID())
if err != nil {
return nil, err
}
ids = append(ids, n.ID())
}
return ids, nil
}
// StopNode stops a node by NodeID.
func (s *Simulation) StopNode(id enode.ID) (err error) {
return s.Net.Stop(id)
}
// StopRandomNode stops a random node.
func (s *Simulation) StopRandomNode() (id enode.ID, err error) {
n := s.Net.GetRandomUpNode()
if n == nil {
return id, ErrNodeNotFound
}
return n.ID(), s.Net.Stop(n.ID())
}
// StopRandomNodes stops random nodes.
func (s *Simulation) StopRandomNodes(count int) (ids []enode.ID, err error) {
ids = make([]enode.ID, 0, count)
for i := 0; i < count; i++ {
n := s.Net.GetRandomUpNode()
if n == nil {
return nil, ErrNodeNotFound
}
err = s.Net.Stop(n.ID())
if err != nil {
return nil, err
}
ids = append(ids, n.ID())
}
return ids, nil
}
// seed the random generator for Simulation.randomNode.
func init() {
rand.Seed(time.Now().UnixNano())
}
// derive a private key for swarm for the node key
// returns the private key used to generate the bzz key
func BzzPrivateKeyFromConfig(conf *adapters.NodeConfig) (*ecdsa.PrivateKey, error) {
// pad the seed key some arbitrary data as ecdsa.GenerateKey takes 40 bytes seed data
privKeyBuf := append(crypto.FromECDSA(conf.PrivateKey), []byte{0x62, 0x7a, 0x7a, 0x62, 0x7a, 0x7a, 0x62, 0x7a}...)
bzzPrivateKey, err := ecdsa.GenerateKey(crypto.S256(), bytes.NewReader(privKeyBuf))
if err != nil {
return nil, err
}
return bzzPrivateKey, nil
}

View File

@ -0,0 +1,446 @@
// Copyright 2018 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/>.
package simulation
import (
"context"
"fmt"
"sync"
"testing"
"time"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/ethereum/go-ethereum/p2p/simulations"
"github.com/ethereum/go-ethereum/p2p/simulations/adapters"
"github.com/ethersphere/swarm/network"
)
func TestUpDownNodeIDs(t *testing.T) {
sim := New(noopServiceFuncMap)
defer sim.Close()
ids, err := sim.AddNodes(10)
if err != nil {
t.Fatal(err)
}
gotIDs := sim.NodeIDs()
if !equalNodeIDs(ids, gotIDs) {
t.Error("returned nodes are not equal to added ones")
}
stoppedIDs, err := sim.StopRandomNodes(3)
if err != nil {
t.Fatal(err)
}
gotIDs = sim.UpNodeIDs()
for _, id := range gotIDs {
if !sim.Net.GetNode(id).Up() {
t.Errorf("node %s should not be down", id)
}
}
if !equalNodeIDs(ids, append(gotIDs, stoppedIDs...)) {
t.Error("returned nodes are not equal to added ones")
}
gotIDs = sim.DownNodeIDs()
for _, id := range gotIDs {
if sim.Net.GetNode(id).Up() {
t.Errorf("node %s should not be up", id)
}
}
if !equalNodeIDs(stoppedIDs, gotIDs) {
t.Error("returned nodes are not equal to the stopped ones")
}
}
func equalNodeIDs(one, other []enode.ID) bool {
if len(one) != len(other) {
return false
}
var count int
for _, a := range one {
var found bool
for _, b := range other {
if a == b {
found = true
break
}
}
if found {
count++
} else {
return false
}
}
return count == len(one)
}
func TestAddNode(t *testing.T) {
sim := New(noopServiceFuncMap)
defer sim.Close()
id, err := sim.AddNode()
if err != nil {
t.Fatal(err)
}
n := sim.Net.GetNode(id)
if n == nil {
t.Fatal("node not found")
}
if !n.Up() {
t.Error("node not started")
}
}
func TestAddNodeWithMsgEvents(t *testing.T) {
sim := New(noopServiceFuncMap)
defer sim.Close()
id, err := sim.AddNode(AddNodeWithMsgEvents(true))
if err != nil {
t.Fatal(err)
}
if !sim.Net.GetNode(id).Config.EnableMsgEvents {
t.Error("EnableMsgEvents is false")
}
id, err = sim.AddNode(AddNodeWithMsgEvents(false))
if err != nil {
t.Fatal(err)
}
if sim.Net.GetNode(id).Config.EnableMsgEvents {
t.Error("EnableMsgEvents is true")
}
}
func TestAddNodeWithService(t *testing.T) {
sim := New(map[string]ServiceFunc{
"noop1": noopServiceFunc,
"noop2": noopServiceFunc,
})
defer sim.Close()
id, err := sim.AddNode(AddNodeWithService("noop1"))
if err != nil {
t.Fatal(err)
}
n := sim.Net.GetNode(id).Node.(*adapters.SimNode)
if n.Service("noop1") == nil {
t.Error("service noop1 not found on node")
}
if n.Service("noop2") != nil {
t.Error("service noop2 should not be found on node")
}
}
func TestAddNodeMultipleServices(t *testing.T) {
sim := New(map[string]ServiceFunc{
"noop1": noopServiceFunc,
"noop2": noopService2Func,
})
defer sim.Close()
id, err := sim.AddNode()
if err != nil {
t.Fatal(err)
}
n := sim.Net.GetNode(id).Node.(*adapters.SimNode)
if n.Service("noop1") == nil {
t.Error("service noop1 not found on node")
}
if n.Service("noop2") == nil {
t.Error("service noop2 not found on node")
}
}
func TestAddNodeDuplicateServiceError(t *testing.T) {
sim := New(map[string]ServiceFunc{
"noop1": noopServiceFunc,
"noop2": noopServiceFunc,
})
defer sim.Close()
wantErr := "duplicate service: *simulation.noopService"
_, err := sim.AddNode()
if err.Error() != wantErr {
t.Errorf("got error %q, want %q", err, wantErr)
}
}
func TestAddNodes(t *testing.T) {
sim := New(noopServiceFuncMap)
defer sim.Close()
nodesCount := 12
ids, err := sim.AddNodes(nodesCount)
if err != nil {
t.Fatal(err)
}
count := len(ids)
if count != nodesCount {
t.Errorf("expected %v nodes, got %v", nodesCount, count)
}
count = len(sim.Net.GetNodes())
if count != nodesCount {
t.Errorf("expected %v nodes, got %v", nodesCount, count)
}
}
func TestAddNodesAndConnectFull(t *testing.T) {
sim := New(noopServiceFuncMap)
defer sim.Close()
n := 12
ids, err := sim.AddNodesAndConnectFull(n)
if err != nil {
t.Fatal(err)
}
simulations.VerifyFull(t, sim.Net, ids)
}
func TestAddNodesAndConnectChain(t *testing.T) {
sim := New(noopServiceFuncMap)
defer sim.Close()
_, err := sim.AddNodesAndConnectChain(12)
if err != nil {
t.Fatal(err)
}
// add another set of nodes to test
// if two chains are connected
_, err = sim.AddNodesAndConnectChain(7)
if err != nil {
t.Fatal(err)
}
simulations.VerifyChain(t, sim.Net, sim.UpNodeIDs())
}
func TestAddNodesAndConnectRing(t *testing.T) {
sim := New(noopServiceFuncMap)
defer sim.Close()
ids, err := sim.AddNodesAndConnectRing(12)
if err != nil {
t.Fatal(err)
}
simulations.VerifyRing(t, sim.Net, ids)
}
func TestAddNodesAndConnectStar(t *testing.T) {
sim := New(noopServiceFuncMap)
defer sim.Close()
ids, err := sim.AddNodesAndConnectStar(12)
if err != nil {
t.Fatal(err)
}
simulations.VerifyStar(t, sim.Net, ids, 0)
}
//To test that uploading a snapshot works
func TestUploadSnapshot(t *testing.T) {
log.Debug("Creating simulation")
s := New(map[string]ServiceFunc{
"bzz": func(ctx *adapters.ServiceContext, b *sync.Map) (node.Service, func(), error) {
addr := network.NewAddr(ctx.Config.Node())
hp := network.NewHiveParams()
hp.Discovery = false
config := &network.BzzConfig{
OverlayAddr: addr.Over(),
UnderlayAddr: addr.Under(),
HiveParams: hp,
}
kad := network.NewKademlia(addr.Over(), network.NewKadParams())
b.Store(BucketKeyKademlia, kad)
return network.NewBzz(config, kad, nil, nil, nil), nil, nil
},
})
defer s.Close()
nodeCount := 16
log.Debug("Uploading snapshot")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
err := s.UploadSnapshot(ctx, fmt.Sprintf("../stream/testing/snapshot_%d.json", nodeCount))
if err != nil {
t.Fatalf("Error uploading snapshot to simulation network: %v", err)
}
log.Debug("Starting simulation...")
s.Run(ctx, func(ctx context.Context, sim *Simulation) error {
log.Debug("Checking")
nodes := sim.UpNodeIDs()
if len(nodes) != nodeCount {
t.Fatal("Simulation network node number doesn't match snapshot node number")
}
return nil
})
log.Debug("Done.")
}
func TestStartStopNode(t *testing.T) {
sim := New(noopServiceFuncMap)
defer sim.Close()
id, err := sim.AddNode()
if err != nil {
t.Fatal(err)
}
n := sim.Net.GetNode(id)
if n == nil {
t.Fatal("node not found")
}
if !n.Up() {
t.Error("node not started")
}
err = sim.StopNode(id)
if err != nil {
t.Fatal(err)
}
if n.Up() {
t.Error("node not stopped")
}
waitForPeerEventPropagation()
err = sim.StartNode(id)
if err != nil {
t.Fatal(err)
}
if !n.Up() {
t.Error("node not started")
}
}
func TestStartStopRandomNode(t *testing.T) {
sim := New(noopServiceFuncMap)
defer sim.Close()
_, err := sim.AddNodes(3)
if err != nil {
t.Fatal(err)
}
id, err := sim.StopRandomNode()
if err != nil {
t.Fatal(err)
}
n := sim.Net.GetNode(id)
if n == nil {
t.Fatal("node not found")
}
if n.Up() {
t.Error("node not stopped")
}
id2, err := sim.StopRandomNode()
if err != nil {
t.Fatal(err)
}
waitForPeerEventPropagation()
idStarted, err := sim.StartRandomNode()
if err != nil {
t.Fatal(err)
}
if idStarted != id && idStarted != id2 {
t.Error("unexpected started node ID")
}
}
func TestStartStopRandomNodes(t *testing.T) {
sim := New(noopServiceFuncMap)
defer sim.Close()
_, err := sim.AddNodes(10)
if err != nil {
t.Fatal(err)
}
ids, err := sim.StopRandomNodes(3)
if err != nil {
t.Fatal(err)
}
for _, id := range ids {
n := sim.Net.GetNode(id)
if n == nil {
t.Fatal("node not found")
}
if n.Up() {
t.Error("node not stopped")
}
}
waitForPeerEventPropagation()
ids, err = sim.StartRandomNodes(2)
if err != nil {
t.Fatal(err)
}
for _, id := range ids {
n := sim.Net.GetNode(id)
if n == nil {
t.Fatal("node not found")
}
if !n.Up() {
t.Error("node not started")
}
}
}
func waitForPeerEventPropagation() {
// Sleep here to ensure that Network.watchPeerEvents defer function
// has set the `node.Up() = false` before we start the node again.
//
// The same node is stopped and started again, and upon start
// watchPeerEvents is started in a goroutine. If the node is stopped
// and then very quickly started, that goroutine may be scheduled later
// then start and force `node.Up() = false` in its defer function.
// This will make this test unreliable.
time.Sleep(1 * time.Second)
}

View File

@ -0,0 +1,65 @@
// Copyright 2018 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/>.
package simulation
import (
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/ethereum/go-ethereum/p2p/simulations/adapters"
)
// Service returns a single Service by name on a particular node
// with provided id.
func (s *Simulation) Service(name string, id enode.ID) node.Service {
simNode, ok := s.Net.GetNode(id).Node.(*adapters.SimNode)
if !ok {
return nil
}
services := simNode.ServiceMap()
if len(services) == 0 {
return nil
}
return services[name]
}
// RandomService returns a single Service by name on a
// randomly chosen node that is up.
func (s *Simulation) RandomService(name string) node.Service {
n := s.Net.GetRandomUpNode().Node.(*adapters.SimNode)
if n == nil {
return nil
}
return n.Service(name)
}
// Services returns all services with a provided name
// from nodes that are up.
func (s *Simulation) Services(name string) (services map[enode.ID]node.Service) {
nodes := s.Net.GetNodes()
services = make(map[enode.ID]node.Service)
for _, node := range nodes {
if !node.Up() {
continue
}
simNode, ok := node.Node.(*adapters.SimNode)
if !ok {
continue
}
services[node.ID()] = simNode.Service(name)
}
return services
}

View File

@ -0,0 +1,46 @@
// Copyright 2018 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/>.
package simulation
import (
"testing"
)
func TestService(t *testing.T) {
sim := New(noopServiceFuncMap)
defer sim.Close()
id, err := sim.AddNode()
if err != nil {
t.Fatal(err)
}
_, ok := sim.Service("noop", id).(*noopService)
if !ok {
t.Fatalf("service is not of %T type", &noopService{})
}
_, ok = sim.RandomService("noop").(*noopService)
if !ok {
t.Fatalf("service is not of %T type", &noopService{})
}
_, ok = sim.Services("noop")[id].(*noopService)
if !ok {
t.Fatalf("service is not of %T type", &noopService{})
}
}

View File

@ -0,0 +1,218 @@
// Copyright 2018 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/>.
package simulation
import (
"context"
"errors"
"net/http"
"sync"
"time"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/p2p/enode"
"github.com/ethereum/go-ethereum/p2p/simulations"
"github.com/ethereum/go-ethereum/p2p/simulations/adapters"
"github.com/ethersphere/swarm/network"
)
// Common errors that are returned by functions in this package.
var (
ErrNodeNotFound = errors.New("node not found")
)
// Simulation provides methods on network, nodes and services
// to manage them.
type Simulation struct {
// Net is exposed as a way to access lower level functionalities
// of p2p/simulations.Network.
Net *simulations.Network
serviceNames []string
cleanupFuncs []func()
buckets map[enode.ID]*sync.Map
shutdownWG sync.WaitGroup
done chan struct{}
mu sync.RWMutex
neighbourhoodSize int
httpSrv *http.Server //attach a HTTP server via SimulationOptions
handler *simulations.Server //HTTP handler for the server
runC chan struct{} //channel where frontend signals it is ready
}
// ServiceFunc is used in New to declare new service constructor.
// The first argument provides ServiceContext from the adapters package
// giving for example the access to NodeID. Second argument is the sync.Map
// where all "global" state related to the service should be kept.
// All cleanups needed for constructed service and any other constructed
// objects should ne provided in a single returned cleanup function.
// Returned cleanup function will be called by Close function
// after network shutdown.
type ServiceFunc func(ctx *adapters.ServiceContext, bucket *sync.Map) (s node.Service, cleanup func(), err error)
// New creates a new simulation instance
// Services map must have unique keys as service names and
// every ServiceFunc must return a node.Service of the unique type.
// This restriction is required by node.Node.Start() function
// which is used to start node.Service returned by ServiceFunc.
func New(services map[string]ServiceFunc) (s *Simulation) {
s = &Simulation{
buckets: make(map[enode.ID]*sync.Map),
done: make(chan struct{}),
neighbourhoodSize: network.NewKadParams().NeighbourhoodSize,
}
adapterServices := make(map[string]adapters.ServiceFunc, len(services))
for name, serviceFunc := range services {
// Scope this variables correctly
// as they will be in the adapterServices[name] function accessed later.
name, serviceFunc := name, serviceFunc
s.serviceNames = append(s.serviceNames, name)
adapterServices[name] = func(ctx *adapters.ServiceContext) (node.Service, error) {
s.mu.Lock()
defer s.mu.Unlock()
b, ok := s.buckets[ctx.Config.ID]
if !ok {
b = new(sync.Map)
}
service, cleanup, err := serviceFunc(ctx, b)
if err != nil {
return nil, err
}
if cleanup != nil {
s.cleanupFuncs = append(s.cleanupFuncs, cleanup)
}
s.buckets[ctx.Config.ID] = b
return service, nil
}
}
s.Net = simulations.NewNetwork(
adapters.NewTCPAdapter(adapterServices),
&simulations.NetworkConfig{ID: "0"},
)
return s
}
// RunFunc is the function that will be called
// on Simulation.Run method call.
type RunFunc func(context.Context, *Simulation) error
// Result is the returned value of Simulation.Run method.
type Result struct {
Duration time.Duration
Error error
}
// Run calls the RunFunc function while taking care of
// cancellation provided through the Context.
func (s *Simulation) Run(ctx context.Context, f RunFunc) (r Result) {
//if the option is set to run a HTTP server with the simulation,
//init the server and start it
start := time.Now()
if s.httpSrv != nil {
log.Info("Waiting for frontend to be ready...(send POST /runsim to HTTP server)")
//wait for the frontend to connect
select {
case <-s.runC:
case <-ctx.Done():
return Result{
Duration: time.Since(start),
Error: ctx.Err(),
}
}
log.Info("Received signal from frontend - starting simulation run.")
}
errc := make(chan error)
quit := make(chan struct{})
defer close(quit)
go func() {
select {
case errc <- f(ctx, s):
case <-quit:
}
}()
var err error
select {
case <-ctx.Done():
err = ctx.Err()
case err = <-errc:
}
return Result{
Duration: time.Since(start),
Error: err,
}
}
// Maximal number of parallel calls to cleanup functions on
// Simulation.Close.
var maxParallelCleanups = 10
// Close calls all cleanup functions that are returned by
// ServiceFunc, waits for all of them to finish and other
// functions that explicitly block shutdownWG
// (like Simulation.PeerEvents) and shuts down the network
// at the end. It is used to clean all resources from the
// simulation.
func (s *Simulation) Close() {
close(s.done)
sem := make(chan struct{}, maxParallelCleanups)
s.mu.RLock()
cleanupFuncs := make([]func(), len(s.cleanupFuncs))
for i, f := range s.cleanupFuncs {
if f != nil {
cleanupFuncs[i] = f
}
}
s.mu.RUnlock()
var cleanupWG sync.WaitGroup
for _, cleanup := range cleanupFuncs {
cleanupWG.Add(1)
sem <- struct{}{}
go func(cleanup func()) {
defer cleanupWG.Done()
defer func() { <-sem }()
cleanup()
}(cleanup)
}
cleanupWG.Wait()
if s.httpSrv != nil {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
err := s.httpSrv.Shutdown(ctx)
if err != nil {
log.Error("Error shutting down HTTP server!", "err", err)
}
close(s.runC)
}
s.shutdownWG.Wait()
s.Net.Shutdown()
}
// Done returns a channel that is closed when the simulation
// is closed by Close method. It is useful for signaling termination
// of all possible goroutines that are created within the test.
func (s *Simulation) Done() <-chan struct{} {
return s.done
}

View File

@ -0,0 +1,203 @@
// Copyright 2018 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/>.
package simulation
import (
"context"
"errors"
"flag"
"sync"
"testing"
"time"
"github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/node"
"github.com/ethereum/go-ethereum/p2p/simulations"
"github.com/ethereum/go-ethereum/p2p/simulations/adapters"
"github.com/mattn/go-colorable"
)
var (
loglevel = flag.Int("loglevel", 2, "verbosity of logs")
)
func init() {
flag.Parse()
log.PrintOrigins(true)
log.Root().SetHandler(log.LvlFilterHandler(log.Lvl(*loglevel), log.StreamHandler(colorable.NewColorableStderr(), log.TerminalFormat(true))))
}
// TestRun tests if Run method calls RunFunc and if it handles context properly.
func TestRun(t *testing.T) {
sim := New(noopServiceFuncMap)
defer sim.Close()
t.Run("call", func(t *testing.T) {
expect := "something"
var got string
r := sim.Run(context.Background(), func(ctx context.Context, sim *Simulation) error {
got = expect
return nil
})
if r.Error != nil {
t.Errorf("unexpected error: %v", r.Error)
}
if got != expect {
t.Errorf("expected %q, got %q", expect, got)
}
})
t.Run("cancellation", func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
r := sim.Run(ctx, func(ctx context.Context, sim *Simulation) error {
time.Sleep(time.Second)
return nil
})
if r.Error != context.DeadlineExceeded {
t.Errorf("unexpected error: %v", r.Error)
}
})
t.Run("context value and duration", func(t *testing.T) {
ctx := context.WithValue(context.Background(), "hey", "there")
sleep := 50 * time.Millisecond
r := sim.Run(ctx, func(ctx context.Context, sim *Simulation) error {
if ctx.Value("hey") != "there" {
return errors.New("expected context value not passed")
}
time.Sleep(sleep)
return nil
})
if r.Error != nil {
t.Errorf("unexpected error: %v", r.Error)
}
if r.Duration < sleep {
t.Errorf("reported run duration less then expected: %s", r.Duration)
}
})
}
// TestClose tests are Close method triggers all close functions and are all nodes not up anymore.
func TestClose(t *testing.T) {
var mu sync.Mutex
var cleanupCount int
sleep := 50 * time.Millisecond
sim := New(map[string]ServiceFunc{
"noop": func(ctx *adapters.ServiceContext, b *sync.Map) (node.Service, func(), error) {
return newNoopService(), func() {
time.Sleep(sleep)
mu.Lock()
defer mu.Unlock()
cleanupCount++
}, nil
},
})
nodeCount := 30
_, err := sim.AddNodes(nodeCount)
if err != nil {
t.Fatal(err)
}
var upNodeCount int
for _, n := range sim.Net.GetNodes() {
if n.Up() {
upNodeCount++
}
}
if upNodeCount != nodeCount {
t.Errorf("all nodes should be up, insted only %v are up", upNodeCount)
}
sim.Close()
if cleanupCount != nodeCount {
t.Errorf("number of cleanups expected %v, got %v", nodeCount, cleanupCount)
}
upNodeCount = 0
for _, n := range sim.Net.GetNodes() {
if n.Up() {
upNodeCount++
}
}
if upNodeCount != 0 {
t.Errorf("all nodes should be down, insted %v are up", upNodeCount)
}
}
// TestDone checks if Close method triggers the closing of done channel.
func TestDone(t *testing.T) {
sim := New(noopServiceFuncMap)
sleep := 50 * time.Millisecond
timeout := 2 * time.Second
start := time.Now()
go func() {
time.Sleep(sleep)
sim.Close()
}()
select {
case <-time.After(timeout):
t.Error("done channel closing timed out")
case <-sim.Done():
if d := time.Since(start); d < sleep {
t.Errorf("done channel closed sooner then expected: %s", d)
}
}
}
// a helper map for usual services that do not do anything
var noopServiceFuncMap = map[string]ServiceFunc{
"noop": noopServiceFunc,
}
// a helper function for most basic noop service
func noopServiceFunc(_ *adapters.ServiceContext, _ *sync.Map) (node.Service, func(), error) {
return newNoopService(), nil, nil
}
func newNoopService() node.Service {
return &noopService{}
}
// a helper function for most basic Noop service
// of a different type then NoopService to test
// multiple services on one node.
func noopService2Func(_ *adapters.ServiceContext, _ *sync.Map) (node.Service, func(), error) {
return new(noopService2), nil, nil
}
// NoopService2 is the service that does not do anything
// but implements node.Service interface.
type noopService2 struct {
simulations.NoopService
}
type noopService struct {
simulations.NoopService
}