diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index a1981eed130fea329ee548be37121df5e66f5163..c162dd6acad0b777b94ebb3b3e7f96eaf4325050 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -12,30 +12,16 @@ jobs: - name: Check out code uses: actions/checkout@v2 - - name: Build GoShimmer Docker network - run: docker-compose -f tools/integration-tests/docker-compose.yml up -d --scale peer_replica=5 --build - - - name: Dispay containers - run: docker ps -a + - name: Build GoShimmer image + run: docker build -t iotaledger/goshimmer . - name: Run integration tests run: docker-compose -f tools/integration-tests/tester/docker-compose.yml up --abort-on-container-exit --exit-code-from tester --build - - name: Stop GoShimmer Docker network - if: always() - run: docker-compose -f tools/integration-tests/docker-compose.yml stop - - - name: Create logs from containers in network + - name: Create logs from tester if: always() run: | - docker logs entry_node > tools/integration-tests/logs/entry_node.log - docker logs peer_master > tools/integration-tests/logs/peer_master.log - docker logs integration-tests_peer_replica_1 > tools/integration-tests/logs/peer_replica_1.log - docker logs integration-tests_peer_replica_2 > tools/integration-tests/logs/peer_replica_2.log - docker logs integration-tests_peer_replica_3 > tools/integration-tests/logs/peer_replica_3.log - docker logs integration-tests_peer_replica_4 > tools/integration-tests/logs/peer_replica_4.log - docker logs integration-tests_peer_replica_5 > tools/integration-tests/logs/peer_replica_5.log - docker logs tester > tools/integration-tests/logs/tester.log + docker logs tester &> tools/integration-tests/logs/tester.log - name: Save logs as artifacts if: always() @@ -46,4 +32,4 @@ jobs: - name: Clean up if: always() - run: docker-compose -f tools/integration-tests/docker-compose.yml down + run: docker-compose -f tools/integration-tests/tester/docker-compose.yml down diff --git a/images/docker-network.png b/images/docker-network.png new file mode 100644 index 0000000000000000000000000000000000000000..cffa7fc8bf4d723f86449e2d2f72a82a69df959a Binary files /dev/null and b/images/docker-network.png differ diff --git a/images/integration-testing-setup.png b/images/integration-testing-setup.png deleted file mode 100644 index 8d59e14b6a61cad794f499b25258c179cd057519..0000000000000000000000000000000000000000 Binary files a/images/integration-testing-setup.png and /dev/null differ diff --git a/images/integration-testing.png b/images/integration-testing.png new file mode 100644 index 0000000000000000000000000000000000000000..9138c7d33aac5f40c560e362688f94efc7f9519a Binary files /dev/null and b/images/integration-testing.png differ diff --git a/tools/docker-network/README.md b/tools/docker-network/README.md new file mode 100644 index 0000000000000000000000000000000000000000..e901f5cdbefebe5448983a3165c07b4ca95c1bc5 --- /dev/null +++ b/tools/docker-network/README.md @@ -0,0 +1,36 @@ +# GoShimmer Network with Docker + + + +Running `docker-compose` spins up a GoShimmer network within Docker as schematically shown in the figure above. +`N` defines the number of `peer_replicas` and can be specified when running the network. +The peers can communicate freely within the Docker network +while the autopeering network visualizer, `master_peer's` dashboard and web API are reachable from the host system on the respective ports. + +The different containers (`entry_node`, `peer_master`, `peer_replica`) are based on the same config file +and separate config file and modified as necessary, respectively. + +## How to use as development tool +Using a standalone throwaway Docker network can be really helpful as a development tool. + +Prerequisites: +- Docker 17.12.0+ +- Docker compose: file format 3.5 + +Reachable from the host system +- autopeering visualizer: http://localhost:9000 +- `master_peer's` dashboard: http: http://localhost:8081 +- `master_peer's` web API: http: http://localhost:8080 + +It is therefore possible to send messages to the local network via the `master_peer` and observe log messages either +via `docker logs --follow CONTAINER` or by starting the Docker network without the `-d` option, as follows. + +``` +docker-compose up --scale peer_replica=5 + +# remove containers and network +docker-compose down +``` + +Sometimes when changing files Docker does not detect the changes on a rebuild. +Then the option `--build` needs to be used with `docker-compose`. \ No newline at end of file diff --git a/tools/integration-tests/config.docker.json b/tools/docker-network/config.docker.json similarity index 100% rename from tools/integration-tests/config.docker.json rename to tools/docker-network/config.docker.json diff --git a/tools/integration-tests/docker-compose.yml b/tools/docker-network/docker-compose.yml similarity index 89% rename from tools/integration-tests/docker-compose.yml rename to tools/docker-network/docker-compose.yml index 068a95ba8f24fff00e6ce3b9903793d7724b4d5e..9735f8cf26e0e28aa7ec0f7b8dc489f9d7b35e3b 100644 --- a/tools/integration-tests/docker-compose.yml +++ b/tools/docker-network/docker-compose.yml @@ -10,7 +10,7 @@ services: volumes: - ./config.docker.json:/config.json:ro ports: - - "9000:9000/tcp" # visualizer + - "127.0.0.1:9000:9000/tcp" # autopeering visualizer expose: - "1888/tcp" # analysis server (within Docker network) networks: @@ -24,8 +24,8 @@ services: volumes: - ./config.docker.json:/config.json:ro ports: - - "8080:8080/tcp" # web API - - "8081:8081/tcp" # dashboard + - "127.0.0.1:8080:8080/tcp" # web API + - "127.0.0.1:8081:8081/tcp" # dashboard depends_on: - entry_node networks: diff --git a/tools/integration-tests/README.md b/tools/integration-tests/README.md index 156b5d2bdcaf80210cc80ab06ccc9450186149f0..c24d2c58cf0ca8e09ebf8a194cbc752d4121affb 100644 --- a/tools/integration-tests/README.md +++ b/tools/integration-tests/README.md @@ -1,53 +1,39 @@ # Integration tests with Docker - + -Running the integration tests spins up a GoShimmer network within Docker as schematically shown in the figure above. -`N` defines the number of `peer_replicas` and can be specified when running the network. -The peers can communicate freely within the Docker network and this is exactly how the tests are run using the `tester` container. -Test can be written in regular Go style while the framework provides convenience functions to access a specific peer's web API or logs. +Running the integration tests spins up a `tester` container within which every test can specify its own GoShimmer network with Docker as schematically shown in the figure above. -The autopeering network visualizer, `master_peer's` dashboard and web API are reachable from the host system on the respective ports. - -The different containers (`entry_node`, `peer_master`, `peer_replica`) load separate config files that can be modified as necessary, respectively. +Peers can communicate freely within their Docker network and this is exactly how the tests are run using the `tester` container. +Test can be written in regular Go style while the framework provides convenience functions to create a new network, access a specific peer's web API or logs. ## How to run Prerequisites: -- Docker -- Docker compose +- Docker 17.12.0+ +- Docker compose: file format 3.5 ``` # Mac & Linux ./runTests.sh ``` -The tests produce `*.log` files for every peer in the `logs` folder after every run. +The tests produce `*.log` files for every networks' peer in the `logs` folder after every run. -Currently, the integration tests are configured to run on every push to GitHub with `peer_replica=5`. -The logs of every peer are stored as artifacts and can be downloaded for closer inspection once the job finishes. +On GitHub logs of every peer are stored as artifacts and can be downloaded for closer inspection once the job finishes. ## Creating tests Tests can be written in regular Go style. Each tested component should reside in its own test file in `tester/tests`. `main_test` with its `TestMain` function is executed before any test in the package and initializes the integration test framework. - -## Use as development tool -Using a standalone throwaway Docker network can be really helpful as a development tool as well. - -Reachable from the host system -- visualizer: http://localhost:9000 -- `master_peer's` dashboard: http: http://localhost:8081 -- `master_peer's` web API: http: http://localhost:8080 - -It is therefore possible to send messages to the local network via the `master_peer` and observe log messages either -via `docker logs --follow CONTAINER` or by starting the Docker network without the `-d` option, as follows. - +Each test has to specify its network where the tests are run. This can be done via the framework at the beginning of a test. +```go +// create a network with name 'testnetwork' with 6 peers and wait until every peer has at least 3 neighbors +n := f.CreateNetwork("testnetwork", 6, 3) +// must be called to create log files and properly clean up +defer n.Shutdown() ``` -docker-compose -f docker-compose.yml up --scale peer_replica=5 - -# 1. test manually with master_peer -# 2. or run in separate terminal window -docker-compose -f tester/docker-compose.yml up --exit-code-from tester -``` -Sometimes when changing files, either in the tests or in GoShimmer, Docker does not detect the changes on a rebuild. -Then the option `--build` needs to be used with `docker-compose`. \ No newline at end of file +## Other tips +Useful for development is to only execute the test you're currently building. For that matter, simply modify the `docker-compose.yml` file as follows: +```yaml +entrypoint: go test ./tests -run <YOUR_TEST_NAME> -v -mod=readonly +``` \ No newline at end of file diff --git a/tools/integration-tests/runTests.sh b/tools/integration-tests/runTests.sh index eb2e4de68a699d772123cab3be5a534d03390053..9f885f01f363c656b2dc6bebdeb4186392c989b3 100755 --- a/tools/integration-tests/runTests.sh +++ b/tools/integration-tests/runTests.sh @@ -1,32 +1,13 @@ #!/bin/bash -if [ -z "$1" ]; then - echo "Usage: `basename $0` number_of_peer_replicas" - exit 0 -fi - -REPLICAS=$1 - -echo "Build GoShimmer Docker network" -docker-compose -f docker-compose.yml up -d --scale peer_replica=$REPLICAS --build -if [ $? -ne 0 ]; then { echo "Failed, aborting." ; exit 1; } fi - -echo "Dispay containers" -docker ps -a +echo "Build GoShimmer image" +docker build -t iotaledger/goshimmer ../../. echo "Run integration tests" docker-compose -f tester/docker-compose.yml up --abort-on-container-exit --exit-code-from tester --build echo "Create logs from containers in network" -docker-compose -f docker-compose.yml stop -docker logs tester > logs/tester.log -docker logs entry_node > logs/entry_node.log -docker logs peer_master > logs/peer_master.log -for (( c=1; c<=$REPLICAS; c++ )) -do -docker logs integration-tests_peer_replica_$c > logs/peer_replica_$c.log -done +docker logs tester &> logs/tester.log echo "Clean up" -docker-compose -f tester/docker-compose.yml down -docker-compose -f docker-compose.yml down +docker-compose -f tester/docker-compose.yml down \ No newline at end of file diff --git a/tools/integration-tests/tester/docker-compose.yml b/tools/integration-tests/tester/docker-compose.yml index dda307f0d1b2d92a9bba8999c814f5e7fe5d797d..62bfbd4c4fbd4ca84e468812e4df34ce06d74c63 100644 --- a/tools/integration-tests/tester/docker-compose.yml +++ b/tools/integration-tests/tester/docker-compose.yml @@ -9,9 +9,5 @@ services: volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - ../../..:/go/src/github.com/iotaledger/goshimmer:ro - networks: - - integration-test + - ../logs:/tmp/logs -networks: - integration-test: - external: true diff --git a/tools/integration-tests/tester/framework/docker.go b/tools/integration-tests/tester/framework/docker.go new file mode 100644 index 0000000000000000000000000000000000000000..1851325ca5a6e91881734cbd5c047f57caa0cddb --- /dev/null +++ b/tools/integration-tests/tester/framework/docker.go @@ -0,0 +1,141 @@ +package framework + +import ( + "context" + "fmt" + "io" + "time" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/strslice" + "github.com/docker/docker/client" + "github.com/docker/go-connections/nat" +) + +// newDockerClient creates a Docker client that communicates via the Docker socket. +func newDockerClient() (*client.Client, error) { + return client.NewClient( + "unix:///var/run/docker.sock", + "", + nil, + nil, + ) +} + +// Wrapper object for a Docker container. +type DockerContainer struct { + client *client.Client + id string +} + +// NewDockerContainer creates a new DockerContainer. +func NewDockerContainer(c *client.Client) *DockerContainer { + return &DockerContainer{client: c} +} + +// NewDockerContainerFromExisting creates a new DockerContainer from an already existing Docker container by name. +func NewDockerContainerFromExisting(c *client.Client, name string) (*DockerContainer, error) { + containers, err := c.ContainerList(context.Background(), types.ContainerListOptions{}) + if err != nil { + return nil, err + } + + for _, cont := range containers { + if cont.Names[0] == name { + return &DockerContainer{ + client: c, + id: cont.ID, + }, nil + } + } + + return nil, fmt.Errorf("could not find container with name '%s'", name) +} + +// CreateGoShimmerEntryNode creates a new container with the GoShimmer entry node's configuration. +func (d *DockerContainer) CreateGoShimmerEntryNode(name string, seed string) error { + containerConfig := &container.Config{ + Image: "iotaledger/goshimmer", + ExposedPorts: nil, + Cmd: strslice.StrSlice{ + fmt.Sprintf("--node.disablePlugins=%s", disabledPluginsEntryNode), + "--autopeering.entryNodes=", + fmt.Sprintf("--autopeering.seed=%s", seed), + }, + } + + return d.CreateContainer(name, containerConfig) +} + +// CreateGoShimmerPeer creates a new container with the GoShimmer peer's configuration. +func (d *DockerContainer) CreateGoShimmerPeer(name string, seed string, entryNodeHost string, entryNodePublicKey string) error { + // configure GoShimmer container instance + containerConfig := &container.Config{ + Image: "iotaledger/goshimmer", + ExposedPorts: nat.PortSet{ + nat.Port("8080/tcp"): {}, + }, + Cmd: strslice.StrSlice{ + fmt.Sprintf("--node.disablePlugins=%s", disabledPluginsPeer), + "--webapi.bindAddress=0.0.0.0:8080", + fmt.Sprintf("--autopeering.seed=%s", seed), + fmt.Sprintf("--autopeering.entryNodes=%s@%s:14626", entryNodePublicKey, entryNodeHost), + }, + } + + return d.CreateContainer(name, containerConfig) +} + +// CreateContainer creates a new container with the given configuration. +func (d *DockerContainer) CreateContainer(name string, containerConfig *container.Config) error { + resp, err := d.client.ContainerCreate(context.Background(), containerConfig, nil, nil, name) + if err != nil { + return err + } + + d.id = resp.ID + return nil +} + +// ConnectToNetwork connects a container to an existent network in the docker host. +func (d *DockerContainer) ConnectToNetwork(networkId string) error { + return d.client.NetworkConnect(context.Background(), networkId, d.id, nil) +} + +// DisconnectFromNetwork disconnects a container from an existent network in the docker host. +func (d *DockerContainer) DisconnectFromNetwork(networkId string) error { + return d.client.NetworkDisconnect(context.Background(), networkId, d.id, true) +} + +// Start sends a request to the docker daemon to start a container. +func (d *DockerContainer) Start() error { + return d.client.ContainerStart(context.Background(), d.id, types.ContainerStartOptions{}) +} + +// Remove kills and removes a container from the docker host. +func (d *DockerContainer) Remove() error { + return d.client.ContainerRemove(context.Background(), d.id, types.ContainerRemoveOptions{Force: true}) +} + +// Stop stops a container without terminating the process. +// The process is blocked until the container stops or the timeout expires. +func (d *DockerContainer) Stop() error { + duration := 10 * time.Second + return d.client.ContainerStop(context.Background(), d.id, &duration) +} + +// Logs returns the logs of the container as io.ReadCloser. +func (d *DockerContainer) Logs() (io.ReadCloser, error) { + options := types.ContainerLogsOptions{ + ShowStdout: true, + ShowStderr: true, + Since: "", + Timestamps: false, + Follow: false, + Tail: "", + Details: false, + } + + return d.client.ContainerLogs(context.Background(), d.id, options) +} diff --git a/tools/integration-tests/tester/framework/framework.go b/tools/integration-tests/tester/framework/framework.go index 58aba94da446c5fea1f2d83512eef940dd5e6793..71d81786ac1e6978d752051f5258f9362facc811 100644 --- a/tools/integration-tests/tester/framework/framework.go +++ b/tools/integration-tests/tester/framework/framework.go @@ -1,99 +1,85 @@ // Package framework provides integration test functionality for GoShimmer with a Docker network. -// It effectively abstracts away all complexity with discovering peers, -// waiting for them to autopeer and offers easy access to the peers' web API -// and logs via Docker. +// It effectively abstracts away all complexity with creating a custom Docker network per test, +// discovering peers, waiting for them to autopeer and offers easy access to the peers' web API and logs. package framework import ( - "fmt" - "math/rand" + "strings" + "sync" "time" "github.com/docker/docker/client" ) -// Framework is a wrapper encapsulating all peers +var ( + once sync.Once + instance *Framework +) + +// Framework is a wrapper that provides the integration testing functionality. type Framework struct { - peers []*Peer - dockerCli *client.Client + tester *DockerContainer + dockerClient *client.Client } -// New creates a new instance of Framework, gets all available peers within the Docker network and -// waits for them to autopeer. -// Panics if no peer is found. -func New() *Framework { - fmt.Printf("Finding available peers...\n") +// Instance returns the singleton Framework instance. +func Instance() (f *Framework, err error) { + once.Do(func() { + f, err = newFramework() + instance = f + }) - cli, err := client.NewClient( - "unix:///var/run/docker.sock", - "", - nil, - nil, - ) + return instance, err +} + +// newFramework creates a new instance of Framework, creates a DockerClient +// and creates a DockerContainer for the tester container where the tests are running in. +func newFramework() (*Framework, error) { + dockerClient, err := newDockerClient() if err != nil { - fmt.Println("Could not create docker CLI client.") - panic(err) + return nil, err } - f := &Framework{ - dockerCli: cli, - peers: getAvailablePeers(cli), + tester, err := NewDockerContainerFromExisting(dockerClient, containerNameTester) + if err != nil { + return nil, err } - if len(f.peers) == 0 { - panic("Could not find any peers in Docker network.") + f := &Framework{ + dockerClient: dockerClient, + tester: tester, } - fmt.Printf("Finding available peers... done. Peers: %v\n", f.peers) - - fmt.Printf("Waiting for autopeering...\n") - f.waitForAutopeering() - fmt.Printf("Waiting for autopeering... done\n") - return f + return f, nil } -// waitForAutopeering waits until all peers have reached a minimum amount of neighbors. -// Panics if this minimum is not reached after autopeeringMaxTries. -func (f *Framework) waitForAutopeering() { - maxTries := autopeeringMaxTries - for maxTries > 0 { +// CreateNetwork creates and returns a (Docker) Network that contains `peers` GoShimmer nodes. +// It waits for the peers to autopeer until the minimum neighbors criteria is met for every peer. +func (f *Framework) CreateNetwork(name string, peers int, minimumNeighbors int) (*Network, error) { + network, err := newNetwork(f.dockerClient, strings.ToLower(name), f.tester) + if err != nil { + return nil, err + } - for _, p := range f.peers { - if resp, err := p.GetNeighbors(false); err != nil { - fmt.Printf("request error: %v\n", err) - } else { - p.SetNeighbors(resp.Chosen, resp.Accepted) - } - } + err = network.createEntryNode() + if err != nil { + return nil, err + } - // verify neighbor requirement - min := 100 - total := 0 - for _, p := range f.peers { - neighbors := p.TotalNeighbors() - if neighbors < min { - min = neighbors - } - total += neighbors + // create peers/GoShimmer nodes + for i := 0; i < peers; i++ { + _, err = network.CreatePeer() + if err != nil { + return nil, err } - if min >= autopeeringMinimumNeighbors { - fmt.Printf("Neighbors: min=%d avg=%.2f\n", min, float64(total)/float64(len(f.peers))) - return - } - - fmt.Println("Not done yet. Try again in 5 seconds...") - time.Sleep(5 * time.Second) - maxTries-- } - panic("Peering not successful.") -} -// Peers returns all available peers. -func (f *Framework) Peers() []*Peer { - return f.peers -} + // wait until containers are fully started + time.Sleep(1 * time.Second) + err = network.WaitForAutopeering(minimumNeighbors) + if err != nil { + return nil, err + } -// RandomPeer returns a random peer out of the list of peers. -func (f *Framework) RandomPeer() *Peer { - return f.peers[rand.Intn(len(f.peers))] + return network, nil } diff --git a/tools/integration-tests/tester/framework/network.go b/tools/integration-tests/tester/framework/network.go new file mode 100644 index 0000000000000000000000000000000000000000..3b4c9480940b9c566749fe4adec05bb32f21532a --- /dev/null +++ b/tools/integration-tests/tester/framework/network.go @@ -0,0 +1,233 @@ +package framework + +import ( + "context" + "crypto/ed25519" + "encoding/base64" + "fmt" + "log" + "math/rand" + "time" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/client" + hive_ed25519 "github.com/iotaledger/hive.go/crypto/ed25519" + "github.com/iotaledger/hive.go/identity" +) + +// Network represents a complete GoShimmer network within Docker. +// Including an entry node and arbitrary many peers. +type Network struct { + id string + name string + + peers []*Peer + tester *DockerContainer + + entryNode *DockerContainer + entryNodeIdentity *identity.Identity + + dockerClient *client.Client +} + +// newNetwork returns a Network instance, creates its underlying Docker network and adds the tester container to the network. +func newNetwork(dockerClient *client.Client, name string, tester *DockerContainer) (*Network, error) { + // create Docker network + resp, err := dockerClient.NetworkCreate(context.Background(), name, types.NetworkCreate{}) + if err != nil { + return nil, err + } + + // the tester container needs to join the Docker network in order to communicate with the peers + err = tester.ConnectToNetwork(resp.ID) + if err != nil { + return nil, err + } + + return &Network{ + id: resp.ID, + name: name, + tester: tester, + dockerClient: dockerClient, + }, nil +} + +// createEntryNode creates the network's entry node. +func (n *Network) createEntryNode() error { + // create identity + publicKey, privateKey, err := hive_ed25519.GenerateKey() + if err != nil { + return err + } + + n.entryNodeIdentity = identity.New(publicKey) + seed := base64.StdEncoding.EncodeToString(ed25519.PrivateKey(privateKey.Bytes()).Seed()) + + // create entry node container + n.entryNode = NewDockerContainer(n.dockerClient) + err = n.entryNode.CreateGoShimmerEntryNode(n.namePrefix(containerNameEntryNode), seed) + if err != nil { + return err + } + err = n.entryNode.ConnectToNetwork(n.id) + if err != nil { + return err + } + err = n.entryNode.Start() + if err != nil { + return err + } + + return nil +} + +// CreatePeer creates a new peer/GoShimmer node in the network and returns it. +func (n *Network) CreatePeer() (*Peer, error) { + name := n.namePrefix(fmt.Sprintf("%s%d", containerNameReplica, len(n.peers))) + + // create identity + publicKey, privateKey, err := hive_ed25519.GenerateKey() + if err != nil { + return nil, err + } + seed := base64.StdEncoding.EncodeToString(ed25519.PrivateKey(privateKey.Bytes()).Seed()) + + // create Docker container + container := NewDockerContainer(n.dockerClient) + err = container.CreateGoShimmerPeer(name, seed, n.namePrefix(containerNameEntryNode), n.entryNodePublicKey()) + if err != nil { + return nil, err + } + err = container.ConnectToNetwork(n.id) + if err != nil { + return nil, err + } + err = container.Start() + if err != nil { + return nil, err + } + + peer := newPeer(name, identity.New(publicKey), container) + n.peers = append(n.peers, peer) + return peer, nil +} + +// Shutdown creates logs and removes network and containers. +// Should always be called when a network is not needed anymore! +func (n *Network) Shutdown() error { + // stop containers + err := n.entryNode.Stop() + if err != nil { + return err + } + for _, p := range n.peers { + err = p.Stop() + if err != nil { + return err + } + } + + // retrieve logs + logs, err := n.entryNode.Logs() + if err != nil { + return err + } + err = createLogFile(n.namePrefix(containerNameEntryNode), logs) + if err != nil { + return err + } + for _, p := range n.peers { + logs, err = p.Logs() + if err != nil { + return err + } + err = createLogFile(p.name, logs) + if err != nil { + return err + } + } + + // remove containers + err = n.entryNode.Remove() + if err != nil { + return err + } + for _, p := range n.peers { + err = p.Remove() + if err != nil { + return err + } + } + + // disconnect tester from network otherwise the network can't be removed + err = n.tester.DisconnectFromNetwork(n.id) + if err != nil { + return err + } + + // remove network + err = n.dockerClient.NetworkRemove(context.Background(), n.id) + if err != nil { + return err + } + + return nil +} + +// WaitForAutopeering waits until all peers have reached the minimum amount of neighbors. +// Returns error if this minimum is not reached after autopeeringMaxTries. +func (n *Network) WaitForAutopeering(minimumNeighbors int) error { + log.Printf("Waiting for autopeering...\n") + defer log.Printf("Waiting for autopeering... done\n") + + for i := autopeeringMaxTries; i > 0; i-- { + + for _, p := range n.peers { + if resp, err := p.GetNeighbors(false); err != nil { + log.Printf("request error: %v\n", err) + } else { + p.SetNeighbors(resp.Chosen, resp.Accepted) + } + } + + // verify neighbor requirement + min := 100 + total := 0 + for _, p := range n.peers { + neighbors := p.TotalNeighbors() + if neighbors < min { + min = neighbors + } + total += neighbors + } + if min >= minimumNeighbors { + log.Printf("Neighbors: min=%d avg=%.2f\n", min, float64(total)/float64(len(n.peers))) + return nil + } + + log.Println("Not done yet. Try again in 5 seconds...") + time.Sleep(5 * time.Second) + } + + return fmt.Errorf("autopeering not successful") +} + +// namePrefix returns the suffix prefixed with the name. +func (n *Network) namePrefix(suffix string) string { + return fmt.Sprintf("%s-%s", n.name, suffix) +} + +// entryNodePublicKey returns the entry node's public key encoded as base64 +func (n *Network) entryNodePublicKey() string { + return base64.StdEncoding.EncodeToString(n.entryNodeIdentity.PublicKey().Bytes()) +} + +// Peers returns all available peers in the network. +func (n *Network) Peers() []*Peer { + return n.peers +} + +// RandomPeer returns a random peer out of the list of peers. +func (n *Network) RandomPeer() *Peer { + return n.peers[rand.Intn(len(n.peers))] +} diff --git a/tools/integration-tests/tester/framework/parameters.go b/tools/integration-tests/tester/framework/parameters.go index a054aa3058393f87cd5c3eac4e59f78ca7462dda..d7c7c938c6f2f036e0268226e388d69a2197a8a4 100644 --- a/tools/integration-tests/tester/framework/parameters.go +++ b/tools/integration-tests/tester/framework/parameters.go @@ -1,11 +1,18 @@ package framework const ( - hostnamePeerMaster = "peer_master" - hostnamePeerReplicaPrefix = "integration-tests_peer_replica_" - - autopeeringMaxTries = 50 - autopeeringMinimumNeighbors = 2 + autopeeringMaxTries = 50 apiPort = "8080" + + containerNameTester = "/tester" + containerNameEntryNode = "entry_node" + containerNameReplica = "replica_" + + logsDir = "/tmp/logs/" + + disabledPluginsEntryNode = "portcheck,spa,analysis,gossip,webapi,webapibroadcastdataendpoint,webapifindtransactionhashesendpoint,webapigetneighborsendpoint,webapigettransactionobjectsbyhashendpoint,webapigettransactiontrytesbyhashendpoint" + disabledPluginsPeer = "portcheck,spa,analysis" + + dockerLogsPrefixLen = 8 ) diff --git a/tools/integration-tests/tester/framework/peer.go b/tools/integration-tests/tester/framework/peer.go index 4df0f64ca16b2bbbac08939bcd3f65aec0b518b6..2ae1774e5578591e4fab441f6f272e20cb6b6d01 100644 --- a/tools/integration-tests/tester/framework/peer.go +++ b/tools/integration-tests/tester/framework/peer.go @@ -1,42 +1,44 @@ package framework import ( - "context" "fmt" - "io" - "net" "net/http" "time" - "github.com/docker/docker/api/types" - dockerclient "github.com/docker/docker/client" - "github.com/iotaledger/goshimmer/client" "github.com/iotaledger/goshimmer/plugins/webapi/autopeering" + "github.com/iotaledger/hive.go/identity" ) // Peer represents a GoShimmer node inside the Docker network type Peer struct { + // name of the GoShimmer instance, Docker container and hostname name string - ip net.IP + // GoShimmer identity + identity *identity.Identity + + // Web API of this peer *client.GoShimmerAPI - dockerCli *dockerclient.Client - chosen []autopeering.Neighbor - accepted []autopeering.Neighbor + + // the DockerContainer that this peer is running in + *DockerContainer + + chosen []autopeering.Neighbor + accepted []autopeering.Neighbor } -// NewPeer creates a new instance of Peer with the given information. -func NewPeer(name string, ip net.IP, dockerCli *dockerclient.Client) *Peer { +// newPeer creates a new instance of Peer with the given information. +func newPeer(name string, identity *identity.Identity, dockerContainer *DockerContainer) *Peer { return &Peer{ - name: name, - ip: ip, - GoShimmerAPI: client.NewGoShimmerAPI(getWebApiBaseUrl(ip), http.Client{Timeout: 30 * time.Second}), - dockerCli: dockerCli, + name: name, + identity: identity, + GoShimmerAPI: client.NewGoShimmerAPI(getWebApiBaseUrl(name), http.Client{Timeout: 30 * time.Second}), + DockerContainer: dockerContainer, } } func (p *Peer) String() string { - return fmt.Sprintf("Peer:{%s, %s, %s, %d}", p.name, p.ip.String(), p.BaseUrl(), p.TotalNeighbors()) + return fmt.Sprintf("Peer:{%s, %s, %s, %d}", p.name, p.identity.ID().String(), p.BaseUrl(), p.TotalNeighbors()) } // TotalNeighbors returns the total number of neighbors the peer has. @@ -52,51 +54,3 @@ func (p *Peer) SetNeighbors(chosen, accepted []autopeering.Neighbor) { p.accepted = make([]autopeering.Neighbor, len(accepted)) copy(p.accepted, accepted) } - -// Logs returns the logs of the peer as io.ReadCloser. -// Logs are returned via Docker and contain every log entry since start of the container/GoShimmer node. -func (p *Peer) Logs() (io.ReadCloser, error) { - return p.dockerCli.ContainerLogs( - context.Background(), - p.name, - types.ContainerLogsOptions{ - ShowStdout: true, - ShowStderr: true, - Since: "", - Timestamps: false, - Follow: false, - Tail: "", - Details: false, - }) -} - -// getAvailablePeers gets all available peers in the Docker network. -// It uses the expected Docker hostnames and tries to resolve them. -// If that does not work it means the host is not available in the network -func getAvailablePeers(dockerCli *dockerclient.Client) (peers []*Peer) { - // get peer master - if addr, err := net.LookupIP(hostnamePeerMaster); err != nil { - fmt.Printf("Could not resolve %s\n", hostnamePeerMaster) - } else { - p := NewPeer(hostnamePeerMaster, addr[0], dockerCli) - peers = append(peers, p) - } - - // get peer replicas - for i := 1; ; i++ { - peerName := fmt.Sprintf("%s%d", hostnamePeerReplicaPrefix, i) - if addr, err := net.LookupIP(peerName); err != nil { - //fmt.Printf("Could not resolve %s\n", peerName) - break - } else { - p := NewPeer(peerName, addr[0], dockerCli) - peers = append(peers, p) - } - } - return -} - -// getWebApiBaseUrl returns the web API base url for the given IP. -func getWebApiBaseUrl(ip net.IP) string { - return fmt.Sprintf("http://%s:%s", ip.String(), apiPort) -} diff --git a/tools/integration-tests/tester/framework/util.go b/tools/integration-tests/tester/framework/util.go new file mode 100644 index 0000000000000000000000000000000000000000..98b5ced90269669d5ee0dd7730cd8b3113e2f6a0 --- /dev/null +++ b/tools/integration-tests/tester/framework/util.go @@ -0,0 +1,41 @@ +package framework + +import ( + "bufio" + "fmt" + "io" + "os" +) + +// getWebApiBaseUrl returns the web API base url for the given IP. +func getWebApiBaseUrl(hostname string) string { + return fmt.Sprintf("http://%s:%s", hostname, apiPort) +} + +// createLogFile creates a log file from the given logs ReadCloser. +func createLogFile(name string, logs io.ReadCloser) error { + defer logs.Close() + + f, err := os.Create(fmt.Sprintf("%s%s.log", logsDir, name)) + if err != nil { + return err + } + defer f.Close() + + // remove non-ascii chars at beginning of line + scanner := bufio.NewScanner(logs) + for scanner.Scan() { + bytes := append(scanner.Bytes()[dockerLogsPrefixLen:], '\n') + _, err = f.Write(bytes) + if err != nil { + return err + } + } + + err = f.Sync() + if err != nil { + return err + } + + return nil +} diff --git a/tools/integration-tests/tester/go.mod b/tools/integration-tests/tester/go.mod index 9f8a2f0478ded6e713f1a7a2c08ecf4d76a7699d..e0be4d0463e4737e878ee9d88c216938cce1d11c 100644 --- a/tools/integration-tests/tester/go.mod +++ b/tools/integration-tests/tester/go.mod @@ -6,9 +6,10 @@ require ( github.com/Microsoft/go-winio v0.4.14 // indirect github.com/docker/distribution v2.7.1+incompatible // indirect github.com/docker/docker v1.13.1 - github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-connections v0.4.0 github.com/docker/go-units v0.4.0 // indirect github.com/iotaledger/goshimmer v0.1.3 + github.com/iotaledger/hive.go v0.0.0-20200403132600-4c10556e08a0 github.com/opencontainers/go-digest v1.0.0-rc1 // indirect github.com/stretchr/testify v1.5.1 ) diff --git a/tools/integration-tests/tester/tests/dockerlogs_test.go b/tools/integration-tests/tester/tests/dockerlogs_test.go index 49189d969b9acbfb3449e017457692c5061880a4..877698d52c5ca24d711c409837ee5ee019597c48 100644 --- a/tools/integration-tests/tester/tests/dockerlogs_test.go +++ b/tools/integration-tests/tester/tests/dockerlogs_test.go @@ -12,9 +12,13 @@ import ( // TestDockerLogs simply verifies that a peer's log message contains "GoShimmer". // Using the combination of logs and regular expressions can be useful to test a certain peer's behavior. func TestDockerLogs(t *testing.T) { + n, err := f.CreateNetwork("TestDockerLogs", 3, 1) + require.NoError(t, err) + defer n.Shutdown() + r := regexp.MustCompile("GoShimmer") - for _, p := range f.Peers() { + for _, p := range n.Peers() { log, err := p.Logs() require.NoError(t, err) diff --git a/tools/integration-tests/tester/tests/main_test.go b/tools/integration-tests/tester/tests/main_test.go index bc82c6c961871b4f798c0c5b3497fdfa5b8f9b2d..4b00cad4796b7004b7fb46070c21870acd42cd0a 100644 --- a/tools/integration-tests/tester/tests/main_test.go +++ b/tools/integration-tests/tester/tests/main_test.go @@ -1,8 +1,8 @@ // Package tests provides the possibility to write integration tests in regular Go style. // The integration test framework is initialized before any test in the package runs and -// thus can readily be used to make requests to peers and read their logs. +// thus can readily be used to create networks. // -// Each tested feature should reside in its own test file and define tests cases as necessary. +// Each tested feature should reside in its own test file and define tests cases and networks as necessary. package tests import ( @@ -17,7 +17,11 @@ var f *framework.Framework // TestMain gets called by the test utility and is executed before any other test in this package. // It is therefore used to initialize the integration testing framework. func TestMain(m *testing.M) { - f = framework.New() + var err error + f, err = framework.Instance() + if err != nil { + panic(err) + } // call the tests os.Exit(m.Run()) diff --git a/tools/integration-tests/tester/tests/relaymessage_test.go b/tools/integration-tests/tester/tests/relaymessage_test.go index ad29b94a609d9f49f7bd8806ef3841d8731bd69a..711eeb3f9ac75289a445349738e33e5ed0a391f0 100644 --- a/tools/integration-tests/tester/tests/relaymessage_test.go +++ b/tools/integration-tests/tester/tests/relaymessage_test.go @@ -11,24 +11,29 @@ import ( // TestRelayMessages checks whether messages are actually relayed/gossiped through the network // by checking the messages' existence on all nodes after a cool down. func TestRelayMessages(t *testing.T) { - numMessages := 100 + n, err := f.CreateNetwork("TestRelayMessages", 6, 3) + require.NoError(t, err) + defer n.Shutdown() + + numMessages := 105 ids := make([]string, numMessages) data := []byte("Test") // create messages on random peers for i := 0; i < numMessages; i++ { - id, err := f.RandomPeer().Data(data) + peer := n.RandomPeer() + id, err := peer.Data(data) require.NoError(t, err) ids[i] = id } // wait for messages to be gossiped - time.Sleep(5 * time.Second) + time.Sleep(10 * time.Second) // check for messages on every peer - for _, peer := range f.Peers() { + for _, peer := range n.Peers() { resp, err := peer.FindMessageById(ids) require.NoError(t, err)