Skip to content

Commit 6c0612c

Browse files
committed
Added integration test based on Arduino Zero
1 parent 2310f8e commit 6c0612c

File tree

5 files changed

+319
-9
lines changed

5 files changed

+319
-9
lines changed

src/client.h

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,30 +71,30 @@ class RPCClient {
7171
MsgPack::arr_size_t resp_size;
7272

7373
if (!unpacker.deserialize(resp_size, r_msg_type, r_msg_id)){
74-
Serial.println("malformed response");
74+
//Serial.println("malformed response");
7575
continue;
7676
};
7777

7878
if ((resp_size.size() != 4) || (r_msg_type != 1) || (r_msg_id != msg_id)){
79-
Serial.println("wrong msg received");
79+
//Serial.println("wrong msg received");
8080
continue;
8181
}
8282

8383
if (!unpacker.unpackable(nil)){
84-
Serial.print("RPC error - ");
84+
//Serial.print("RPC error - ");
8585
if (!unpacker.deserialize(rpc_error, nil)){
86-
Serial.println("wrong error msg received");
86+
//Serial.println("wrong error msg received");
8787
continue;
8888
}
89-
Serial.print(" error code: ");
90-
Serial.print(rpc_error.code);
91-
Serial.print(" error str: ");
92-
Serial.println(rpc_error.traceback);
89+
//Serial.print(" error code: ");
90+
//Serial.print(rpc_error.code);
91+
//Serial.print(" error str: ");
92+
//Serial.println(rpc_error.traceback);
9393
msg_id += 1;
9494
flush_buffer();
9595
return false;
9696
} else if (!unpacker.deserialize(nil, result)){
97-
Serial.println("Unexpected result");
97+
//Serial.println("Unexpected result");
9898
continue;
9999
}
100100
break;
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
#include <RPClite.h>
2+
3+
#ifdef ARDUINO_SAMD_ZERO
4+
#define MSGPACKRPC Serial // MsgPack RPC runs on the hardware serial port (that do not disconnects on reset/upload)
5+
#define DEBUG SerialUSB // Debug and upload port is the native USB
6+
#else
7+
#error "Unsupported board"
8+
#endif
9+
10+
SerialTransport transport(&MSGPACKRPC);
11+
RPCClient rpc(transport);
12+
13+
void setup() {
14+
MSGPACKRPC.begin(115200);
15+
DEBUG.begin(115200);
16+
17+
while (!DEBUG) { /* WAIT for serial port to connect */ }
18+
19+
testSuccessfulCallFl64();
20+
testWrongCall();
21+
testSuccessfulCallBool();
22+
// testWrongCall();
23+
}
24+
25+
void testSuccessfulCallFl64() {
26+
float result;
27+
DEBUG.println("mult(2.0, 3.0)");
28+
bool ok = rpc.call("mult", result, 2.0, 3.0);
29+
DEBUG.print("-> ");
30+
if (ok) {
31+
DEBUG.println(result);
32+
} else {
33+
DEBUG.println("error");
34+
}
35+
}
36+
37+
void testSuccessfulCallBool() {
38+
bool result;
39+
DEBUG.println("or(true, false)");
40+
bool ok = rpc.call("or", result, true, false);
41+
DEBUG.print("-> ");
42+
if (ok) {
43+
DEBUG.println(result ? "true" : "false");
44+
} else {
45+
DEBUG.println("error");
46+
}
47+
48+
DEBUG.println("or(false)");
49+
ok = rpc.call("or", result, false);
50+
DEBUG.print("-> ");
51+
if (ok) {
52+
DEBUG.println(result ? "true" : "false");
53+
} else {
54+
DEBUG.println("error");
55+
}
56+
}
57+
58+
void testWrongCall() {
59+
float result;
60+
DEBUG.println("mult(2.0)");
61+
bool ok = rpc.call("mult", result, 2.0);
62+
DEBUG.print("-> ");
63+
if (ok) {
64+
DEBUG.println(result);
65+
} else {
66+
DEBUG.println("error");
67+
}
68+
}
69+
70+
void loop() {
71+
}
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
package RpcClientZeroTest
2+
3+
import (
4+
"encoding/hex"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"os"
9+
"testing"
10+
"time"
11+
12+
"github.com/arduino/go-paths-helper"
13+
"github.com/stretchr/testify/require"
14+
"github.com/vmihailenco/msgpack/v5"
15+
"go.bug.st/serial"
16+
)
17+
18+
func TestBasicComm(t *testing.T) {
19+
// Get the upload port to upload the sketch
20+
fqbn, _, uploadPort := getFQBNAndPorts(t)
21+
{
22+
// Upload the sketch
23+
cli, err := paths.NewProcess(nil, "arduino-cli", "compile", "--fqbn", fqbn, "--library", "../..", "-u", "-p", uploadPort)
24+
require.NoError(t, err)
25+
cli.RedirectStderrTo(os.Stderr)
26+
cli.RedirectStdoutTo(os.Stdout)
27+
require.NoError(t, cli.Run())
28+
}
29+
30+
// Get the rpc and debug ports
31+
fqbn2, rpcPort, debugPort := getFQBNAndPorts(t)
32+
require.Equal(t, fqbn, fqbn2, "FQBN mismatch between upload and run ports: %s != %s", fqbn, fqbn2)
33+
34+
// Connect to the RPC serial port
35+
_rpcSer, err := serial.Open(rpcPort, &serial.Mode{BaudRate: 115200})
36+
rpcSer := &DebugStream{upstream: _rpcSer, portname: rpcPort}
37+
require.NoError(t, err)
38+
t.Cleanup(func() { rpcSer.Close() })
39+
in := msgpack.NewDecoder(rpcSer)
40+
out := msgpack.NewEncoder(rpcSer)
41+
out.UseCompactInts(true)
42+
43+
// Connect to the Debug serial port
44+
debugSer, err := serial.Open(debugPort, &serial.Mode{BaudRate: 115200})
45+
require.NoError(t, err)
46+
t.Cleanup(func() { debugSer.Close() })
47+
expectDebug := func(exp string) {
48+
buff := make([]byte, len(exp))
49+
read := 0
50+
for read < len(exp) {
51+
n, err := debugSer.Read(buff[read:])
52+
read += n
53+
require.NoError(t, err)
54+
}
55+
require.Equal(t, exp, string(buff))
56+
}
57+
58+
// Timeout fallback: close the connection after 10 seconds, if the test do not go through
59+
go func() {
60+
time.Sleep(10 * time.Second)
61+
rpcSer.Close()
62+
debugSer.Close()
63+
}()
64+
65+
// RPC: Receive an RPC call to the "mult" method with 2 arguments
66+
// and send back the result
67+
t.Run("RPCClientCallFloatArgs", func(t *testing.T) {
68+
arr, err := in.DecodeSlice()
69+
require.NoError(t, err)
70+
require.Equal(t, []any{int8(0), int8(1), "mult", []any{2.0, 3.0}}, arr)
71+
err = out.Encode([]any{1, 1, nil, 6.0})
72+
require.NoError(t, err)
73+
expectDebug("mult(2.0, 3.0)\r\n")
74+
expectDebug("-> 6.00\r\n")
75+
})
76+
77+
// RPC: Receive an RPC call to the "mult" method with 1 argument (wrong number of arguments)
78+
// and send back an error with [int, string] format
79+
t.Run("RPCClientCallFloatArgsError", func(t *testing.T) {
80+
arr, err := in.DecodeSlice()
81+
require.NoError(t, err)
82+
require.Equal(t, []any{int8(0), int8(2), "mult", []any{2.0}}, arr)
83+
err = out.Encode([]any{1, 2, []any{1, "missing parameter"}, nil})
84+
require.NoError(t, err)
85+
expectDebug("mult(2.0)\r\n")
86+
expectDebug("-> error\r\n")
87+
})
88+
89+
// RPC: Receive an RPC call to the "or" method with 1 or 2 arguments
90+
// and send back the result
91+
t.Run("RPCClientCallBoolArgs", func(t *testing.T) {
92+
arr, err := in.DecodeSlice()
93+
require.NoError(t, err)
94+
require.Equal(t, []any{int8(0), int8(3), "or", []any{true, false}}, arr)
95+
err = out.Encode([]any{1, 3, nil, true})
96+
require.NoError(t, err)
97+
expectDebug("or(true, false)\r\n")
98+
expectDebug("-> true\r\n")
99+
100+
arr, err = in.DecodeSlice()
101+
require.NoError(t, err)
102+
require.Equal(t, []any{int8(0), int8(4), "or", []any{false}}, arr)
103+
err = out.Encode([]any{1, 4, nil, false})
104+
require.NoError(t, err)
105+
expectDebug("or(false)\r\n")
106+
expectDebug("-> false\r\n")
107+
})
108+
109+
// RPC: Receive an RPC call to the "mult" method with 1 argument (wrong number of arguments)
110+
// and send back a custom error without [int, string] format
111+
// t.Run("RPCClientCallFloatArgsErrorCustom", func(t *testing.T) {
112+
// arr, err := in.DecodeSlice()
113+
// require.NoError(t, err)
114+
// require.Equal(t, []any{int8(0), int8(3), "mult", []any{2.0}}, arr)
115+
// err = out.Encode([]any{1, 3, "missing parameter", nil})
116+
// require.NoError(t, err)
117+
// expectDebug("mult(2.0)\r\n")
118+
// expectDebug("-> error\r\n")
119+
// })
120+
}
121+
122+
func getFQBNAndPorts(t *testing.T) (fqbn string, rpcPort string, uploadPort string) {
123+
cli, err := paths.NewProcess(nil, "arduino-cli", "board", "list", "--json")
124+
require.NoError(t, err)
125+
out, _, err := cli.RunAndCaptureOutput(t.Context())
126+
require.NoError(t, err)
127+
var cliResult struct {
128+
DetectedPorts []struct {
129+
MatchingBoards []struct {
130+
Fqbn string `json:"fqbn"`
131+
} `json:"matching_boards"`
132+
Port struct {
133+
Address string `json:"address"`
134+
} `json:"port"`
135+
} `json:"detected_ports"`
136+
}
137+
require.NoError(t, json.Unmarshal(out, &cliResult))
138+
checkFQBN := func(boardFQBN string) {
139+
if fqbn != boardFQBN {
140+
fqbn = boardFQBN
141+
uploadPort = ""
142+
rpcPort = ""
143+
}
144+
}
145+
for _, port := range cliResult.DetectedPorts {
146+
for _, board := range port.MatchingBoards {
147+
if board.Fqbn == "arduino:samd:arduino_zero_edbg" {
148+
checkFQBN("arduino:samd:arduino_zero_native")
149+
rpcPort = port.Port.Address
150+
}
151+
if board.Fqbn == "arduino:samd:arduino_zero_native" {
152+
checkFQBN(board.Fqbn)
153+
uploadPort = port.Port.Address
154+
}
155+
}
156+
}
157+
require.NotEmpty(t, uploadPort, "Upload port not found")
158+
require.NotEmpty(t, rpcPort, "Debug port not found")
159+
return fqbn, rpcPort, uploadPort
160+
}
161+
162+
// DebugStream is a wrapper around io.ReadWriteCloser that logs the data
163+
// read and written to the stream in hex format.
164+
// It is used to debug the communication with the Arduino board.
165+
type DebugStream struct {
166+
upstream io.ReadWriteCloser
167+
portname string
168+
}
169+
170+
func (d *DebugStream) Read(p []byte) (n int, err error) {
171+
n, err = d.upstream.Read(p)
172+
if err != nil {
173+
fmt.Printf("%s READ ERROR: %v\n", d.portname, err)
174+
} else {
175+
fmt.Printf("%s READ << %s\n", d.portname, hex.EncodeToString(p[:n]))
176+
}
177+
return n, err
178+
}
179+
180+
func (d *DebugStream) Write(p []byte) (n int, err error) {
181+
n, err = d.upstream.Write(p)
182+
if err != nil {
183+
fmt.Printf("%s WRITE ERROR: %v\n", d.portname, err)
184+
} else {
185+
fmt.Printf("%s WRITE >> %s\n", d.portname, hex.EncodeToString(p[:n]))
186+
}
187+
return n, err
188+
}
189+
190+
func (d *DebugStream) Close() error {
191+
err := d.upstream.Close()
192+
fmt.Printf("%s CLOSE", d.portname)
193+
if err != nil {
194+
fmt.Printf(" (ERROR: %v)", err)
195+
}
196+
fmt.Println()
197+
return err
198+
}

test/RpcClientTest/go.mod

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
module RpcClientZeroTest
2+
3+
go 1.24.2
4+
5+
require (
6+
github.com/arduino/go-paths-helper v1.13.1
7+
github.com/stretchr/testify v1.8.4
8+
github.com/vmihailenco/msgpack/v5 v5.4.1
9+
go.bug.st/serial v1.6.4
10+
)
11+
12+
require (
13+
github.com/creack/goselect v0.1.2 // indirect
14+
github.com/davecgh/go-spew v1.1.1 // indirect
15+
github.com/pmezard/go-difflib v1.0.0 // indirect
16+
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
17+
golang.org/x/sys v0.32.0 // indirect
18+
gopkg.in/yaml.v3 v3.0.1 // indirect
19+
)

test/RpcClientTest/go.sum

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
github.com/arduino/go-paths-helper v1.13.1 h1:M7SCdLB2VldxOdChnjZkxAZwWZdDtNY4IlHL9nxGQFo=
2+
github.com/arduino/go-paths-helper v1.13.1/go.mod h1:dDodKn2ZX4iwuoBMapdDO+5d0oDLBeM4BS0xS4i40Ak=
3+
github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0=
4+
github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
5+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
6+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
7+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
8+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
9+
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
10+
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
11+
github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8=
12+
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
13+
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
14+
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
15+
go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A=
16+
go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI=
17+
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
18+
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
19+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
20+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
21+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
22+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

0 commit comments

Comments
 (0)