diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..13bf87d6 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,19 @@ +name: tests + +on: [push, pull_request] + +jobs: + CI: + name: tests + runs-on: ubuntu-latest + container: guerra1994/go-mqtt-docker-env + + steps: + - name: check out code + uses: actions/checkout@v2 + + - name: download mod + run: go mod download + + - name: run functional tests + run: ./scripts/functional-tests.sh \ No newline at end of file diff --git a/.gitignore b/.gitignore index ca80c31f..1ccb4b1d 100644 --- a/.gitignore +++ b/.gitignore @@ -58,4 +58,7 @@ arduino-connector-arm .idea setup_host_test_env.sh -# End \ No newline at end of file +### Remote vscode container +.devcontainer/* + +# End diff --git a/README.md b/README.md index 1991e50e..5da40da7 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,15 @@ go get github.com/sanbornm/go-selfupdate See [API](./API.md) +## Functional tests + +This type of tests are executed all locally and you need to configure a dedicated docker container: +- `docker pull guerra1994/go-mqtt-docker-env` +- follow the steps [here](https://hub.docker.com/r/guerra1994/go-mqtt-docker-env) to run it +- run mosquitto broker in background mode using `mosquitto &` +- then run your test, for example `go test -v --run="TestDockerPsApi"` + + ## Integration tests disclaimer You will see in the following paragraphs that the testing environment and procedures are strictly coupled with the diff --git a/go.mod b/go.mod index 50cb7bf5..6bcdccb8 100644 --- a/go.mod +++ b/go.mod @@ -51,7 +51,7 @@ require ( github.com/shirou/gopsutil v2.17.13-0.20180801053943-8048a2e9c577+incompatible github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 // indirect github.com/sirupsen/logrus v1.1.0 // indirect - github.com/stretchr/testify v1.2.2 + github.com/stretchr/testify v1.3.0 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 golang.org/x/net v0.0.0-20190311183353-d8887717615a golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 // indirect diff --git a/go.sum b/go.sum index 78cdac24..dfe82bea 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,7 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/containerd/continuity v0.0.0-20181003075958-be9bd761db19 h1:HSgjWPBWohO3kHDPwCPUGSLqJjXCjA7ad5057beR2ZU= github.com/containerd/continuity v0.0.0-20181003075958-be9bd761db19/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/docker/cli v0.0.0-20180905184309-44371c7c34d5 h1:FGNHsOn20/i4y8Ck+qQ8rXSN9j7IuBhqcMC+HGpbTHE= @@ -121,8 +122,12 @@ github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 h1:udFKJ0aHUL60LboW/A+D github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc= github.com/sirupsen/logrus v1.1.0 h1:65VZabgUiV9ktjGM5nTq0+YurgTyX+YI2lSSfDjI+qU= github.com/sirupsen/logrus v1.1.0/go.mod h1:zrgwTnHtNr00buQ1vSptGe8m1f/BbgsPukg8qsT7A+A= +github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793 h1:u+LnwYTOOW7Ukr/fppxEb1Nwz0AtPflrblfvUudpo+I= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= diff --git a/handlers.go b/handlers.go index 622f3bef..cabc0794 100644 --- a/handlers.go +++ b/handlers.go @@ -131,7 +131,7 @@ func (status *Status) UploadEvent(client mqtt.Client, msg mqtt.Message) { if _, err = os.Stat(sketchPath); !os.IsNotExist(err) { err = os.Remove(sketchPath) if err != nil { - status.Error("/upload", errors.Wrapf(err, "remove %d", sketch.Name)) + status.Error("/upload", errors.Wrapf(err, "remove %s", sketch.Name)) return } } diff --git a/handlers_containers.go b/handlers_containers.go index b94bdf13..e39af14f 100644 --- a/handlers_containers.go +++ b/handlers_containers.go @@ -1,7 +1,7 @@ // // This file is part of arduino-connector // -// Copyright (C) 2017-2018 Arduino AG (http://www.arduino.cc/) +// Copyright (C) 2017-2020 Arduino AG (http://www.arduino.cc/) // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -37,7 +37,7 @@ import ( "github.com/docker/docker/api/types/filters" "github.com/docker/docker/api/types/network" docker "github.com/docker/docker/client" - "github.com/eclipse/paho.mqtt.golang" + mqtt "github.com/eclipse/paho.mqtt.golang" "github.com/pkg/errors" "github.com/shirou/gopsutil/host" "golang.org/x/net/context" @@ -71,7 +71,7 @@ type ChangeNamePayload struct { ContainerName string `json:"name"` } -// ContainersPsEvent implements docker ps -a +// ContainersPsEvent send info about "docker ps -a" command func (s *Status) ContainersPsEvent(client mqtt.Client, msg mqtt.Message) { psPayload := PsPayload{} err := json.Unmarshal(msg.Payload(), &psPayload) @@ -91,13 +91,15 @@ func (s *Status) ContainersPsEvent(client mqtt.Client, msg mqtt.Message) { return } - // Send result data, err := json.Marshal(containers) if err != nil { s.Error("/containers/ps", fmt.Errorf("Json marsahl result: %s", err)) return } - s.Info("/containers/ps", string(data)+"\n") + + if !s.SendInfo(s.topicPertinence+"/containers/ps", string(data)+"\n") { + fmt.Println("error sending info") + } } // ContainersListImagesEvent implements docker images diff --git a/handlers_containers_test.go b/handlers_containers_test.go index 8792504f..eb9dff57 100644 --- a/handlers_containers_test.go +++ b/handlers_containers_test.go @@ -1,7 +1,7 @@ // // This file is part of arduino-connector // -// Copyright (C) 2017-2018 Arduino AG (http://www.arduino.cc/) +// Copyright (C) 2017-2020 Arduino AG (http://www.arduino.cc/) // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -22,16 +22,63 @@ import ( "encoding/json" "fmt" "os" + "os/exec" "strings" "testing" "time" "github.com/docker/docker/api/types" + docker "github.com/docker/docker/client" + mqtt "github.com/eclipse/paho.mqtt.golang" "github.com/stretchr/testify/assert" ) -// tests +func TestDockerPsApi(t *testing.T) { + ui := NewMqttTestClientLocal() + defer ui.Close() + + status := NewStatus(program{}.Config, nil, nil, "") + status.dockerClient, _ = docker.NewClientWithOpts(docker.WithVersion("1.38")) + acOptions := mqtt.NewClientOptions().AddBroker("tcp://localhost:1883").SetClientID("arduino-connector") + status.mqttClient = mqtt.NewClient(acOptions) + + if token := status.mqttClient.Connect(); token.Wait() && token.Error() != nil { + t.Fatal(token.Error()) + } + + subscribeTopic(status.mqttClient, "0", "/containers/ps/post", status, status.ContainersPsEvent, false) + + resp := ui.MqttSendAndReceiveTimeout(t, "/containers/ps", "{}", 50*time.Millisecond) + + // ask Docker about containers effectively running + cmd := exec.Command("bash", "-c", "docker ps -a") + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatal(err) + } + + lines := strings.Split(string(out), "\n") + // Remove the first line (command output header) and the last line (empty line) + lines = lines[1 : len(lines)-1] + + // Take json without INFO tag + resp = strings.TrimPrefix(resp, "INFO: ") + resp = strings.TrimSuffix(resp, "\n\n") + var result []types.Container + if err := json.Unmarshal([]byte(resp), &result); err != nil { + t.Fatal(err) + } + + assert.Equal(t, len(result), len(lines)) + for i, line := range lines { + containerId := strings.Fields(line)[0] + assert.True(t, strings.HasPrefix(result[i].ID, containerId)) + } + + status.mqttClient.Disconnect(100) +} + func TestConnectorProcessIsRunning(t *testing.T) { outputMessage, err := ExecAsVagrantSshCmd("systemctl status ArduinoConnector | grep running") if err != nil { diff --git a/handlers_test.go b/handlers_test.go index 8915ebee..8dc75203 100644 --- a/handlers_test.go +++ b/handlers_test.go @@ -30,9 +30,8 @@ import ( "testing" "time" + mqtt "github.com/eclipse/paho.mqtt.golang" "github.com/stretchr/testify/assert" - - "github.com/eclipse/paho.mqtt.golang" ) // ExecAsVagrantSshCmd "wraps vagrant ssh -c @@ -52,6 +51,19 @@ type MqttTestClient struct { thingToTestId string } +func NewMqttTestClientLocal() *MqttTestClient { + uiOptions := mqtt.NewClientOptions().AddBroker("tcp://localhost:1883").SetClientID("UI") + ui := mqtt.NewClient(uiOptions) + if token := ui.Connect(); token.Wait() && token.Error() != nil { + panic(token.Error()) + } + + return &MqttTestClient{ + ui, + "", + } +} + func NewMqttTestClient() *MqttTestClient { cert := "test/cert.pem" key := "test/privateKey.pem" @@ -105,6 +117,36 @@ func (tmc *MqttTestClient) Close() { tmc.client.Disconnect(100) } +func (tmc *MqttTestClient) MqttSendAndReceiveTimeout(t *testing.T, topic, request string, timeout time.Duration) string { + t.Helper() + + respChan := make(chan string) + if token := tmc.client.Subscribe(topic, 0, func(client mqtt.Client, msg mqtt.Message) { + respChan <- string(msg.Payload()) + }); token.Wait() && token.Error() != nil { + t.Fatal(token.Error()) + } + + postTopic := strings.Join([]string{topic, "post"}, "/") + if token := tmc.client.Publish(postTopic, 0, false, request); token.Wait() && token.Error() != nil { + t.Fatal(token.Error()) + } + + select { + case <-time.After(timeout): + if token := tmc.client.Unsubscribe(topic); token.Wait() && token.Error() != nil { + t.Fatal(token.Error()) + } + close(respChan) + + t.Fatalf("MqttSendAndReceiveTimeout() timeout for topic %s", topic) + + return "" + case resp := <-respChan: + return resp + } +} + func (tmc *MqttTestClient) MqttSendAndReceiveSync(t *testing.T, topic, request string) string { iotTopic := strings.Join([]string{"$aws/things", tmc.thingToTestId, topic}, "/") diff --git a/main.go b/main.go index eead6205..f87554f3 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,7 @@ // // This file is part of arduino-connector // -// Copyright (C) 2017-2018 Arduino AG (http://www.arduino.cc/) +// Copyright (C) 2017-2020 Arduino AG (http://www.arduino.cc/) // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -215,7 +215,7 @@ func (p program) run() { } // Create global status - status := NewStatus(p.Config, nil, nil) + status := NewStatus(p.Config, nil, nil, "$aws/things/"+p.Config.ID) status.Update(p.Config) // Setup MQTT connection @@ -330,10 +330,9 @@ func subscribeTopics(mqttClient mqtt.Client, id string, status *Status) { subscribeTopic(mqttClient, id, "/containers/rename/post", status, status.ContainersRenameEvent, true) } -func subscribeTopic(mqttClient mqtt.Client, id, topic string, status *Status, statusHandler mqtt.MessageHandler, isWriteFsRequiredForTopic bool) { +func subscribeTopic(client mqtt.Client, id, topic string, s *Status, statusHandler mqtt.MessageHandler, isWriteFsRequiredForTopic bool) { handler := statusHandler - - if status.config.CheckRoFs && isWriteFsRequiredForTopic { + if s.config.CheckRoFs && isWriteFsRequiredForTopic { handler = func(client mqtt.Client, msg mqtt.Message) { mountRootFilesystemRw() statusHandler(client, msg) @@ -341,14 +340,20 @@ func subscribeTopic(mqttClient mqtt.Client, id, topic string, status *Status, st } } + completeTopic := s.topicPertinence + topic if debugMqtt { debugHandler := func(client mqtt.Client, msg mqtt.Message) { fmt.Println("MQTT IN:", string(msg.Topic()), string(msg.Payload())) handler(client, msg) } - mqttClient.Subscribe("$aws/things/"+id+topic, 1, debugHandler) + + if token := client.Subscribe(completeTopic, 1, debugHandler); token.Wait() && token.Error() != nil { + fmt.Println(token.Error()) + } } else { - mqttClient.Subscribe("$aws/things/"+id+topic, 1, handler) + if token := client.Subscribe(completeTopic, 1, handler); token.Wait() && token.Error() != nil { + fmt.Println(token.Error()) + } } } diff --git a/scripts/functional-tests.sh b/scripts/functional-tests.sh new file mode 100755 index 00000000..d8b0ab75 --- /dev/null +++ b/scripts/functional-tests.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +set -euo pipefail + +trap 'kill "$(pidof mosquitto)"' EXIT + +mosquitto > /dev/null & +go test -v --run="TestDockerPsApi" \ No newline at end of file diff --git a/status.go b/status.go index 53e4bbc0..8c2f38bc 100644 --- a/status.go +++ b/status.go @@ -1,7 +1,7 @@ // // This file is part of arduino-connector // -// Copyright (C) 2017-2018 Arduino AG (http://www.arduino.cc/) +// Copyright (C) 2017-2020 Arduino AG (http://www.arduino.cc/) // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -26,19 +26,20 @@ import ( "time" docker "github.com/docker/docker/client" - "github.com/eclipse/paho.mqtt.golang" + mqtt "github.com/eclipse/paho.mqtt.golang" "github.com/pkg/errors" ) // Status contains info about the sketches running on the device type Status struct { - config Config - id string - mqttClient mqtt.Client - dockerClient docker.APIClient - Sketches map[string]*SketchStatus `json:"sketches"` - messagesSent int - firstMessageAt time.Time + config Config + id string + mqttClient mqtt.Client + dockerClient docker.APIClient + Sketches map[string]*SketchStatus `json:"sketches"` + messagesSent int + firstMessageAt time.Time + topicPertinence string } // SketchBinding represents a pair (SketchName,SketchId) @@ -64,13 +65,14 @@ type Endpoint struct { } // NewStatus creates a new status that publishes on a topic -func NewStatus(config Config, mqttClient mqtt.Client, dockerClient docker.APIClient) *Status { +func NewStatus(config Config, mqttClient mqtt.Client, dockerClient docker.APIClient, topicPertinence string) *Status { return &Status{ - config: config, - id: config.ID, - mqttClient: mqttClient, - dockerClient: dockerClient, - Sketches: map[string]*SketchStatus{}, + config: config, + id: config.ID, + mqttClient: mqttClient, + dockerClient: dockerClient, + Sketches: map[string]*SketchStatus{}, + topicPertinence: topicPertinence, } } @@ -122,6 +124,25 @@ func (s *Status) Info(topic, msg string) bool { return res } +// SendInfo send information to a specific topic +func (s *Status) SendInfo(topic, msg string) bool { + if s.mqttClient == nil { + return false + } + + s.messagesSent++ + + if token := s.mqttClient.Publish(topic, 0, false, "INFO: "+msg+"\n"); token.Wait() && token.Error() != nil { + fmt.Println(token.Error()) + } + + if debugMqtt { + fmt.Println("MQTT OUT: "+topic, "INFO: "+msg+"\n") + } + + return true +} + // Raw sends a message on the specified topic without further processing func (s *Status) Raw(topic, msg string) { if s.mqttClient == nil {