Skip to content

Commit 5be93c5

Browse files
Improve the arduino-app-cli version command by adding the "server version" #31 (#49)
* Add the server version to arduino-app-cli version. * Add copyright header. --------- Co-authored-by: Luca Rinaldi <[email protected]>
1 parent 606a084 commit 5be93c5

File tree

2 files changed

+194
-8
lines changed

2 files changed

+194
-8
lines changed

cmd/arduino-app-cli/version/version.go

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,34 +16,95 @@
1616
package version
1717

1818
import (
19+
"encoding/json"
1920
"fmt"
21+
"net"
22+
"net/http"
23+
"net/url"
24+
"time"
2025

2126
"github.com/spf13/cobra"
2227

2328
"github.com/arduino/arduino-app-cli/cmd/feedback"
2429
)
2530

26-
func NewVersionCmd(version string) *cobra.Command {
31+
// The actual listening address for the daemon
32+
// is defined in the installation package
33+
const (
34+
DefaultHostname = "localhost"
35+
DefaultPort = "8800"
36+
ProgramName = "Arduino App CLI"
37+
)
38+
39+
func NewVersionCmd(clientVersion string) *cobra.Command {
2740
cmd := &cobra.Command{
2841
Use: "version",
2942
Short: "Print the version number of Arduino App CLI",
3043
Run: func(cmd *cobra.Command, args []string) {
31-
feedback.PrintResult(versionResult{
32-
AppName: "Arduino App CLI",
33-
Version: version,
34-
})
44+
port, _ := cmd.Flags().GetString("port")
45+
46+
daemonVersion, err := getDaemonVersion(http.Client{}, port)
47+
if err != nil {
48+
feedback.Warnf("Warning: cannot get the running daemon version on %s:%s\n", DefaultHostname, port)
49+
}
50+
51+
result := versionResult{
52+
Name: ProgramName,
53+
Version: clientVersion,
54+
DaemonVersion: daemonVersion,
55+
}
56+
57+
feedback.PrintResult(result)
3558
},
3659
}
60+
cmd.Flags().String("port", DefaultPort, "The daemon network port")
3761
return cmd
3862
}
3963

64+
func getDaemonVersion(httpClient http.Client, port string) (string, error) {
65+
66+
httpClient.Timeout = time.Second
67+
68+
url := url.URL{
69+
Scheme: "http",
70+
Host: net.JoinHostPort(DefaultHostname, port),
71+
Path: "/v1/version",
72+
}
73+
74+
resp, err := httpClient.Get(url.String())
75+
if err != nil {
76+
return "", err
77+
}
78+
defer resp.Body.Close()
79+
80+
if resp.StatusCode != http.StatusOK {
81+
return "", fmt.Errorf("unexpected status code received")
82+
}
83+
84+
var daemonResponse struct {
85+
Version string `json:"version"`
86+
}
87+
if err := json.NewDecoder(resp.Body).Decode(&daemonResponse); err != nil {
88+
return "", err
89+
}
90+
91+
return daemonResponse.Version, nil
92+
}
93+
4094
type versionResult struct {
41-
AppName string `json:"appName"`
42-
Version string `json:"version"`
95+
Name string `json:"name"`
96+
Version string `json:"version"`
97+
DaemonVersion string `json:"daemon_version,omitempty"`
4398
}
4499

45100
func (r versionResult) String() string {
46-
return fmt.Sprintf("%s v%s", r.AppName, r.Version)
101+
resultMessage := fmt.Sprintf("%s version %s", ProgramName, r.Version)
102+
103+
if r.DaemonVersion != "" {
104+
resultMessage = fmt.Sprintf("%s\ndaemon version: %s",
105+
resultMessage, r.DaemonVersion)
106+
}
107+
return resultMessage
47108
}
48109

49110
func (r versionResult) Data() interface{} {
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
// This file is part of arduino-app-cli.
2+
//
3+
// Copyright 2025 ARDUINO SA (http://www.arduino.cc/)
4+
//
5+
// This software is released under the GNU General Public License version 3,
6+
// which covers the main part of arduino-app-cli.
7+
// The terms of this license can be found at:
8+
// https://www.gnu.org/licenses/gpl-3.0.en.html
9+
//
10+
// You can be released from the requirements of the above licenses by purchasing
11+
// a commercial license. Buying such a license is mandatory if you want to
12+
// modify or otherwise use the software for commercial activities involving the
13+
// Arduino software without disclosing the source code of your own applications.
14+
// To purchase a commercial license, send an email to [email protected].
15+
16+
package version
17+
18+
import (
19+
"errors"
20+
"io"
21+
"net/http"
22+
"strings"
23+
"testing"
24+
25+
"github.com/stretchr/testify/require"
26+
)
27+
28+
func TestDaemonVersion(t *testing.T) {
29+
testCases := []struct {
30+
name string
31+
serverStub Tripper
32+
port string
33+
expectedResult string
34+
expectedErrorMessage string
35+
}{
36+
{
37+
name: "return the server version when the server is up",
38+
serverStub: successServer,
39+
port: "8800",
40+
expectedResult: "3.0-server",
41+
expectedErrorMessage: "",
42+
},
43+
{
44+
name: "return error if default server is not listening on default port",
45+
serverStub: failureServer,
46+
port: "8800",
47+
expectedResult: "",
48+
expectedErrorMessage: `Get "http://localhost:8800/v1/version": connection refused`,
49+
},
50+
{
51+
name: "return error if provided server is not listening on provided port",
52+
serverStub: failureServer,
53+
port: "1234",
54+
expectedResult: "",
55+
expectedErrorMessage: `Get "http://localhost:1234/v1/version": connection refused`,
56+
},
57+
{
58+
name: "return error for server response 500 Internal Server Error",
59+
serverStub: failureInternalServerError,
60+
port: "0",
61+
expectedResult: "",
62+
expectedErrorMessage: "unexpected status code received",
63+
},
64+
65+
{
66+
name: "return error for server up and wrong json response",
67+
serverStub: successServerWrongJson,
68+
port: "8800",
69+
expectedResult: "",
70+
expectedErrorMessage: "invalid character '<' looking for beginning of value",
71+
},
72+
}
73+
74+
for _, tc := range testCases {
75+
t.Run(tc.name, func(t *testing.T) {
76+
// arrange
77+
httpClient := http.Client{}
78+
httpClient.Transport = tc.serverStub
79+
80+
// act
81+
result, err := getDaemonVersion(httpClient, tc.port)
82+
83+
// assert
84+
require.Equal(t, tc.expectedResult, result)
85+
if err != nil {
86+
require.Equal(t, tc.expectedErrorMessage, err.Error())
87+
}
88+
})
89+
}
90+
}
91+
92+
// Leverage the http.Client's RoundTripper
93+
// to return a canned response and bypass network calls.
94+
type Tripper func(*http.Request) (*http.Response, error)
95+
96+
func (t Tripper) RoundTrip(request *http.Request) (*http.Response, error) {
97+
return t(request)
98+
}
99+
100+
var successServer = Tripper(func(*http.Request) (*http.Response, error) {
101+
body := io.NopCloser(strings.NewReader(`{"version":"3.0-server"}`))
102+
return &http.Response{
103+
StatusCode: http.StatusOK,
104+
Body: body,
105+
}, nil
106+
})
107+
108+
var successServerWrongJson = Tripper(func(*http.Request) (*http.Response, error) {
109+
body := io.NopCloser(strings.NewReader(`<!doctype html><html lang="en"`))
110+
return &http.Response{
111+
StatusCode: http.StatusOK,
112+
Body: body,
113+
}, nil
114+
})
115+
116+
var failureServer = Tripper(func(*http.Request) (*http.Response, error) {
117+
return nil, errors.New("connection refused")
118+
})
119+
120+
var failureInternalServerError = Tripper(func(*http.Request) (*http.Response, error) {
121+
return &http.Response{
122+
StatusCode: http.StatusInternalServerError,
123+
Body: io.NopCloser(strings.NewReader("")),
124+
}, nil
125+
})

0 commit comments

Comments
 (0)