diff --git a/extras/integration_test/README.md b/extras/integration_test/README.md new file mode 100644 index 0000000..9cd7732 --- /dev/null +++ b/extras/integration_test/README.md @@ -0,0 +1,133 @@ +## Integration test for the RPC library + +This is a test that runs on the Arduino Zero board. It could be easily extended to other boards with multiple serial ports. + +### Running the test + +Prerequisites: + +* An Arduino Zero board, connected with both the USB ports. +* `arduino-cli` +* The platform `arduino:samd` installed through the `arduino-cli`. +* The dependencies of the RPClite library installed through the `arduino-cli`. +* A working `go` programming language compiler. + +To run the test, from a terminal change directory to this folder, and run: + +`go test -v` + +it should compile and upload the test sketch, and perform the RPC call tests. + +The output should look similar to the following: + +``` +RpcLite/test/RpcClientTest$ go test -v +=== RUN TestBasicComm +Lo sketch usa 27028 byte (10%) dello spazio disponibile per i programmi. Il massimo è 262144 byte. +Le variabili globali usano 4040 byte (12%) di memoria dinamica, lasciando altri 28728 byte liberi per le variabili locali. Il massimo è 32768 byte. +Atmel SMART device 0x10010005 found +Device : ATSAMD21G18A +Chip ID : 10010005 +Version : v2.0 [Arduino:XYZ] Apr 11 2019 13:09:49 +Address : 8192 +Pages : 3968 +Page Size : 64 bytes +Total Size : 248KB +Planes : 1 +Lock Regions : 16 +Locked : none +Security : false +Boot Flash : true +BOD : true +BOR : true +Arduino : FAST_CHIP_ERASE +Arduino : FAST_MULTI_PAGE_WRITE +Arduino : CAN_CHECKSUM_MEMORY_BUFFER +Erase flash +done in 0.873 seconds + +Write 27028 bytes to flash (423 pages) +[==============================] 100% (423/423 pages) +done in 0.155 seconds + +Verify 27028 bytes of flash with checksum. +Verify successful +done in 0.026 seconds +CPU reset. +=== RUN TestBasicComm/RPCClientCallFloatArgs +/dev/ttyACM0 READ << 94 +/dev/ttyACM0 READ << 0001 +/dev/ttyACM0 READ << a4 +/dev/ttyACM0 READ << 6d75 +/dev/ttyACM0 READ << 6c +/dev/ttyACM0 READ << 74 +/dev/ttyACM0 READ << 92cb +/dev/ttyACM0 READ << 40 +/dev/ttyACM0 READ << 0000 +/dev/ttyACM0 READ << 00 +/dev/ttyACM0 READ << 0000 +/dev/ttyACM0 READ << 00 +/dev/ttyACM0 READ << 00cb +/dev/ttyACM0 READ << 40 +/dev/ttyACM0 READ << 08 +/dev/ttyACM0 READ << 0000 +/dev/ttyACM0 READ << 00 +/dev/ttyACM0 READ << 0000 +/dev/ttyACM0 READ << 00 +/dev/ttyACM0 WRITE >> 94 +/dev/ttyACM0 WRITE >> 01 +/dev/ttyACM0 WRITE >> 01 +/dev/ttyACM0 WRITE >> c0 +/dev/ttyACM0 WRITE >> cb4018000000000000 +=== RUN TestBasicComm/RPCClientCallFloatArgsError +/dev/ttyACM0 READ << 9400 +/dev/ttyACM0 READ << 02a46d +/dev/ttyACM0 READ << 75 +/dev/ttyACM0 READ << 6c74 +/dev/ttyACM0 READ << 91 +/dev/ttyACM0 READ << cb40 +/dev/ttyACM0 READ << 00 +/dev/ttyACM0 READ << 00 +/dev/ttyACM0 READ << 0000 +/dev/ttyACM0 READ << 00 +/dev/ttyACM0 READ << 0000 +/dev/ttyACM0 WRITE >> 94 +/dev/ttyACM0 WRITE >> 01 +/dev/ttyACM0 WRITE >> 02 +/dev/ttyACM0 WRITE >> 92 +/dev/ttyACM0 WRITE >> 01 +/dev/ttyACM0 WRITE >> b1 +/dev/ttyACM0 WRITE >> 6d697373696e6720706172616d65746572 +/dev/ttyACM0 WRITE >> c0 +=== RUN TestBasicComm/RPCClientCallBoolArgs +/dev/ttyACM0 READ << 9400 +/dev/ttyACM0 READ << 03 +/dev/ttyACM0 READ << a26f +/dev/ttyACM0 READ << 72 +/dev/ttyACM0 READ << 92c3 +/dev/ttyACM0 READ << c2 +/dev/ttyACM0 WRITE >> 94 +/dev/ttyACM0 WRITE >> 01 +/dev/ttyACM0 WRITE >> 03 +/dev/ttyACM0 WRITE >> c0 +/dev/ttyACM0 WRITE >> c3 +/dev/ttyACM0 READ << 9400 +/dev/ttyACM0 READ << 04 +/dev/ttyACM0 READ << a26f +/dev/ttyACM0 READ << 72 +/dev/ttyACM0 READ << 91 +/dev/ttyACM0 READ << c2 +/dev/ttyACM0 WRITE >> 94 +/dev/ttyACM0 WRITE >> 01 +/dev/ttyACM0 WRITE >> 04 +/dev/ttyACM0 WRITE >> c0 +/dev/ttyACM0 WRITE >> c2 +/dev/ttyACM0 CLOSE +--- PASS: TestBasicComm (10.21s) + --- PASS: TestBasicComm/RPCClientCallFloatArgs (0.03s) + --- PASS: TestBasicComm/RPCClientCallFloatArgsError (0.03s) + --- PASS: TestBasicComm/RPCClientCallBoolArgs (0.01s) +PASS +ok RpcClientZeroTest 10.216s +RpcLite/test/RpcClientTest$ +``` \ No newline at end of file diff --git a/extras/integration_test/RPCClient_test.go b/extras/integration_test/RPCClient_test.go new file mode 100644 index 0000000..8836470 --- /dev/null +++ b/extras/integration_test/RPCClient_test.go @@ -0,0 +1,112 @@ +package testsuite + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/vmihailenco/msgpack/v5" + "go.bug.st/serial" +) + +func TestRPCClient(t *testing.T) { + // Get the upload port to upload the sketch + rpcPort, debugPort := UploadSketchAndGetRPCAndDebugPorts(t) + + // Connect to the RPC serial port + _rpcSer, err := serial.Open(rpcPort, &serial.Mode{BaudRate: 115200}) + rpcSer := &DebugStream{Upstream: _rpcSer, Portname: rpcPort} + require.NoError(t, err) + t.Cleanup(func() { rpcSer.Close() }) + in := msgpack.NewDecoder(rpcSer) + out := msgpack.NewEncoder(rpcSer) + out.UseCompactInts(true) + + // Connect to the Debug serial port + debugSer, err := serial.Open(debugPort, &serial.Mode{BaudRate: 115200}) + require.NoError(t, err) + t.Cleanup(func() { debugSer.Close() }) + expectDebug := func(s string) { Expect(t, debugSer, s) } + + // Timeout fallback: close the connection after 10 seconds, if the test do not go through + go func() { + time.Sleep(10 * time.Second) + rpcSer.Close() + debugSer.Close() + }() + + msgID := 0 + + // 1: Receive an RPC call to the "mult" method with 2 arguments + // and send back the result + t.Run("RPCClientCallFloatArgs", func(t *testing.T) { + arr, err := in.DecodeSlice() + require.NoError(t, err) + require.Equal(t, []any{int8(0), int8(msgID), "mult", []any{2.0, 3.0}}, arr) + err = out.Encode([]any{1, msgID, nil, 6.0}) + require.NoError(t, err) + expectDebug("mult(2.0, 3.0)\r\n") + expectDebug("-> 6.00\r\n") + msgID++ + }) + + // 2: Receive an RPC call to the "mult" method with 1 argument (wrong number of arguments) + // and send back an error with [int, string] format + t.Run("RPCClientCallFloatArgsError", func(t *testing.T) { + arr, err := in.DecodeSlice() + require.NoError(t, err) + require.Equal(t, []any{int8(0), int8(msgID), "mult", []any{2.0}}, arr) + err = out.Encode([]any{1, msgID, []any{1, "missing parameter"}, nil}) + require.NoError(t, err) + expectDebug("mult(2.0)\r\n") + expectDebug("-> error\r\n") + msgID++ + }) + + // 3, 4: Receive an RPC call to the "or" method with 1 or 2 arguments + // and send back the result + t.Run("RPCClientCallBoolArgs", func(t *testing.T) { + arr, err := in.DecodeSlice() + require.NoError(t, err) + require.Equal(t, []any{int8(0), int8(msgID), "or", []any{true, false}}, arr) + err = out.Encode([]any{1, msgID, nil, true}) + require.NoError(t, err) + expectDebug("or(true, false)\r\n") + expectDebug("-> true\r\n") + msgID++ + + arr, err = in.DecodeSlice() + require.NoError(t, err) + require.Equal(t, []any{int8(0), int8(msgID), "or", []any{false}}, arr) + err = out.Encode([]any{1, msgID, nil, false}) + require.NoError(t, err) + expectDebug("or(false)\r\n") + expectDebug("-> false\r\n") + msgID++ + }) + + // 5: Receive an RPC call to the "mult" method with 1 argument (wrong number of arguments) + // and send back an error with [int, string] format with a long string + t.Run("RPCClientCallFloatArgsErrorWithLongString", func(t *testing.T) { + arr, err := in.DecodeSlice() + require.NoError(t, err) + require.Equal(t, []any{int8(0), int8(msgID), "mult", []any{2.0}}, arr) + err = out.Encode([]any{1, msgID, []any{2, "method get_led_state not available"}, nil}) + require.NoError(t, err) + expectDebug("mult(2.0)\r\n") + expectDebug("-> error\r\n") + msgID++ + }) + + // RPC: Receive an RPC call to the "mult" method with 1 argument (wrong number of arguments) + // and send back a custom error without [int, string] format + // t.Run("RPCClientCallFloatArgsErrorCustom", func(t *testing.T) { + // arr, err := in.DecodeSlice() + // require.NoError(t, err) + // require.Equal(t, []any{int8(0), int8(3), "mult", []any{2.0}}, arr) + // err = out.Encode([]any{1, 3, "missing parameter", nil}) + // require.NoError(t, err) + // expectDebug("mult(2.0)\r\n") + // expectDebug("-> error\r\n") + // }) +} diff --git a/extras/integration_test/RPCServer_test.go b/extras/integration_test/RPCServer_test.go new file mode 100644 index 0000000..5fbe9d8 --- /dev/null +++ b/extras/integration_test/RPCServer_test.go @@ -0,0 +1,93 @@ +package testsuite + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + "github.com/vmihailenco/msgpack/v5" + "go.bug.st/serial" +) + +func TestRPCServer(t *testing.T) { + // Get the upload port to upload the sketch + rpcPort, debugPort := UploadSketchAndGetRPCAndDebugPorts(t) + + // Connect to the RPC serial port + _rpcSer, err := serial.Open(rpcPort, &serial.Mode{BaudRate: 115200}) + rpcSer := &DebugStream{Upstream: _rpcSer, Portname: rpcPort} + require.NoError(t, err) + t.Cleanup(func() { rpcSer.Close() }) + in := msgpack.NewDecoder(rpcSer) + out := msgpack.NewEncoder(rpcSer) + out.UseCompactInts(true) + + // Connect to the Debug serial port + debugSer, err := serial.Open(debugPort, &serial.Mode{BaudRate: 115200}) + require.NoError(t, err) + t.Cleanup(func() { debugSer.Close() }) + expectDebug := func(s string) { Expect(t, debugSer, s) } + + // Timeout fallback: close the connection after 10 seconds, if the test do not go through + go func() { + time.Sleep(10 * time.Second) + rpcSer.Close() + debugSer.Close() + }() + + // 1: Send an RPC call to the "add" method with 2 arguments + // and get back the result + t.Run("RPCServerCallIntArgs", func(t *testing.T) { + err = out.Encode([]any{0, 1, "add", []any{2, 3}}) + require.NoError(t, err) + expectDebug("add(2, 3)\r\n") + arr, err := in.DecodeSlice() + require.NoError(t, err) + require.Equal(t, []any{int8(1), int8(1), nil, int8(5)}, arr) + }) + + // 2, 3: Send the same RPC call with 1 and 3 arguments, and get back the error + t.Run("RPCServerCallWrongIntArgsCount", func(t *testing.T) { + err = out.Encode([]any{0, 2, "add", []any{2}}) + require.NoError(t, err) + arr, err := in.DecodeSlice() + require.NoError(t, err) + require.Equal(t, []any{ + int8(1), + int8(2), + []any{uint8(253), "Missing call parameters (WARNING: Default param resolution is not implemented)"}, + nil, + }, arr) + + err = out.Encode([]any{0, 3, "add", []any{2, 3, 4}}) + require.NoError(t, err) + arr, err = in.DecodeSlice() + require.NoError(t, err) + require.Equal(t, []any{ + int8(1), + int8(3), + []any{uint8(253), "Too many parameters"}, + nil, + }, arr) + }) + + // 4: Send an RPC call to the "greet" method + t.Run("RPCServerCallNoArgsReturnString", func(t *testing.T) { + err = out.Encode([]any{0, 4, "greet", []any{}}) + require.NoError(t, err) + expectDebug("greet()\r\n") + arr, err := in.DecodeSlice() + require.NoError(t, err) + require.Equal(t, []any{int8(1), int8(4), nil, "Hello World!"}, arr) + }) + + // 5: Send an RPC call to the "loopback" method with 1 string argument + t.Run("RPCServerCallStringArgsReturnString", func(t *testing.T) { + err = out.Encode([]any{0, 5, "loopback", []any{"Hello World!"}}) + require.NoError(t, err) + expectDebug("loopback(\"Hello World!\")\r\n") + arr, err := in.DecodeSlice() + require.NoError(t, err) + require.Equal(t, []any{int8(1), int8(5), nil, "Hello World!"}, arr) + }) +} diff --git a/extras/integration_test/TestRPCClient/TestRPCClient.ino b/extras/integration_test/TestRPCClient/TestRPCClient.ino new file mode 100644 index 0000000..7785909 --- /dev/null +++ b/extras/integration_test/TestRPCClient/TestRPCClient.ino @@ -0,0 +1,83 @@ +#include + +#ifdef ARDUINO_SAMD_ZERO +#define MSGPACKRPC Serial // MsgPack RPC runs on the hardware serial port (that do not disconnects on reset/upload) +#define DEBUG SerialUSB // Debug and upload port is the native USB +#elif ARDUINO_GIGA +#define MSGPACKRPC Serial1 // MsgPack RPC runs on Serial1 +#define DEBUG SerialUSB // Debug and upload port is Serial +#elif ARDUINO_NANO_RP2040_CONNECT +#define MSGPACKRPC Serial1 // MsgPack RPC runs on Serial1 +#define DEBUG SerialUSB // Debug and upload port is Serial +#else +#error "Unsupported board" +#endif + +SerialTransport transport(&MSGPACKRPC); +RPCClient rpc(transport); + +void setup() { + MSGPACKRPC.begin(115200); + DEBUG.begin(115200); + + while (!DEBUG) { /* WAIT for serial port to connect */ } + + // 1 + testSuccessfulCallFl64(); + // 2 + testWrongCall(); + // 3, 4 + testSuccessfulCallBool(); + // 5 + testWrongCall(); + + // testWrongCall(); +} + +void testSuccessfulCallFl64() { + float result; + DEBUG.println("mult(2.0, 3.0)"); + bool ok = rpc.call("mult", result, 2.0, 3.0); + DEBUG.print("-> "); + if (ok) { + DEBUG.println(result); + } else { + DEBUG.println("error"); + } +} + +void testSuccessfulCallBool() { + bool result; + DEBUG.println("or(true, false)"); + bool ok = rpc.call("or", result, true, false); + DEBUG.print("-> "); + if (ok) { + DEBUG.println(result ? "true" : "false"); + } else { + DEBUG.println("error"); + } + + DEBUG.println("or(false)"); + ok = rpc.call("or", result, false); + DEBUG.print("-> "); + if (ok) { + DEBUG.println(result ? "true" : "false"); + } else { + DEBUG.println("error"); + } +} + +void testWrongCall() { + float result; + DEBUG.println("mult(2.0)"); + bool ok = rpc.call("mult", result, 2.0); + DEBUG.print("-> "); + if (ok) { + DEBUG.println(result); + } else { + DEBUG.println("error"); + } +} + +void loop() { +} diff --git a/extras/integration_test/TestRPCClient/serial_ports.h b/extras/integration_test/TestRPCClient/serial_ports.h new file mode 100644 index 0000000..3c47da5 --- /dev/null +++ b/extras/integration_test/TestRPCClient/serial_ports.h @@ -0,0 +1,14 @@ +// This file is automatically generated from ../serial_ports.h. DO NOT EDIT. + +#ifdef ARDUINO_SAMD_ZERO +#define MSGPACKRPC Serial // MsgPack RPC runs on the hardware serial port (that do not disconnects on reset/upload) +#define DEBUG SerialUSB // Debug and upload port is the native USB +#elif ARDUINO_GIGA +#define MSGPACKRPC Serial1 // MsgPack RPC runs on Serial1 +#define DEBUG SerialUSB // Debug and upload port is Serial +#elif ARDUINO_NANO_RP2040_CONNECT +#define MSGPACKRPC Serial1 // MsgPack RPC runs on Serial1 +#define DEBUG SerialUSB // Debug and upload port is Serial +#else +#error "Unsupported board" +#endif diff --git a/extras/integration_test/TestRPCServer/TestRPCServer.ino b/extras/integration_test/TestRPCServer/TestRPCServer.ino new file mode 100644 index 0000000..8ba1342 --- /dev/null +++ b/extras/integration_test/TestRPCServer/TestRPCServer.ino @@ -0,0 +1,41 @@ +#include +#include "serial_ports.h" + +SerialTransport transport(&MSGPACKRPC); +RPCServer server(transport); + +int add(int a, int b) { + DEBUG.print("add("); + DEBUG.print(a); + DEBUG.print(", "); + DEBUG.print(b); + DEBUG.println(")"); + return a+b; +} + +MsgPack::str_t greet() { + DEBUG.println("greet()"); + return MsgPack::str_t ("Hello World!"); +} + +MsgPack::str_t loopback(MsgPack::str_t message){ + DEBUG.print("loopback(\""); + DEBUG.print(message); + DEBUG.println("\")"); + return message; +} + +void setup() { + MSGPACKRPC.begin(115200); + DEBUG.begin(115200); + transport.begin(); + server.bind("add", add); + server.bind("greet", greet); + server.bind("loopback", loopback); + + while (!DEBUG) { /* WAIT for serial port to connect */ } +} + +void loop() { + server.run(); +} diff --git a/extras/integration_test/TestRPCServer/serial_ports.h b/extras/integration_test/TestRPCServer/serial_ports.h new file mode 100644 index 0000000..3c47da5 --- /dev/null +++ b/extras/integration_test/TestRPCServer/serial_ports.h @@ -0,0 +1,14 @@ +// This file is automatically generated from ../serial_ports.h. DO NOT EDIT. + +#ifdef ARDUINO_SAMD_ZERO +#define MSGPACKRPC Serial // MsgPack RPC runs on the hardware serial port (that do not disconnects on reset/upload) +#define DEBUG SerialUSB // Debug and upload port is the native USB +#elif ARDUINO_GIGA +#define MSGPACKRPC Serial1 // MsgPack RPC runs on Serial1 +#define DEBUG SerialUSB // Debug and upload port is Serial +#elif ARDUINO_NANO_RP2040_CONNECT +#define MSGPACKRPC Serial1 // MsgPack RPC runs on Serial1 +#define DEBUG SerialUSB // Debug and upload port is Serial +#else +#error "Unsupported board" +#endif diff --git a/extras/integration_test/go.mod b/extras/integration_test/go.mod new file mode 100644 index 0000000..aac91de --- /dev/null +++ b/extras/integration_test/go.mod @@ -0,0 +1,19 @@ +module ArduinoRPCliteTestSuite + +go 1.24.2 + +require ( + github.com/arduino/go-paths-helper v1.13.1 + github.com/stretchr/testify v1.8.4 + github.com/vmihailenco/msgpack/v5 v5.4.1 + go.bug.st/serial v1.6.4 +) + +require ( + github.com/creack/goselect v0.1.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + golang.org/x/sys v0.32.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/extras/integration_test/go.sum b/extras/integration_test/go.sum new file mode 100644 index 0000000..9289a06 --- /dev/null +++ b/extras/integration_test/go.sum @@ -0,0 +1,22 @@ +github.com/arduino/go-paths-helper v1.13.1 h1:M7SCdLB2VldxOdChnjZkxAZwWZdDtNY4IlHL9nxGQFo= +github.com/arduino/go-paths-helper v1.13.1/go.mod h1:dDodKn2ZX4iwuoBMapdDO+5d0oDLBeM4BS0xS4i40Ak= +github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= +github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= +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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A= +go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/extras/integration_test/serial_ports.h b/extras/integration_test/serial_ports.h new file mode 100644 index 0000000..f0c738b --- /dev/null +++ b/extras/integration_test/serial_ports.h @@ -0,0 +1,13 @@ + +#ifdef ARDUINO_SAMD_ZERO +#define MSGPACKRPC Serial // MsgPack RPC runs on the hardware serial port (that do not disconnects on reset/upload) +#define DEBUG SerialUSB // Debug and upload port is the native USB +#elif ARDUINO_GIGA +#define MSGPACKRPC Serial1 // MsgPack RPC runs on Serial1 +#define DEBUG SerialUSB // Debug and upload port is Serial +#elif ARDUINO_NANO_RP2040_CONNECT +#define MSGPACKRPC Serial1 // MsgPack RPC runs on Serial1 +#define DEBUG SerialUSB // Debug and upload port is Serial +#else +#error "Unsupported board" +#endif diff --git a/extras/integration_test/testsuite.go b/extras/integration_test/testsuite.go new file mode 100644 index 0000000..d783640 --- /dev/null +++ b/extras/integration_test/testsuite.go @@ -0,0 +1,161 @@ +package testsuite + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "io" + "os" + "testing" + + "github.com/arduino/go-paths-helper" + "github.com/stretchr/testify/require" +) + +// UploadSketchAndGetRPCAndDebugPorts uploads the sketch to the board and +// returns the RPC and debug ports. The sketch is supposed to be +// located in the current directory. +func UploadSketchAndGetRPCAndDebugPorts(t *testing.T) (rpc string, debug string) { + sketchName := t.Name() + fqbn, _, uploadPort := getFQBNAndPorts(t) + { + // Copy serial_ports.h file to the sketch directory + serialPortHeader := []byte(fmt.Sprintln("// This file is automatically generated from ../serial_ports.h. DO NOT EDIT.")) + serialPortsH, err := paths.New("serial_ports.h").ReadFile() + require.NoError(t, err) + err = paths.New(sketchName, "serial_ports.h").WriteFile(append(serialPortHeader, serialPortsH...)) + require.NoError(t, err) + + // Upload the sketch + cli, err := paths.NewProcess(nil, "arduino-cli", "compile", "--fqbn", fqbn, "--library", "../..", "-u", "-p", uploadPort, sketchName) + require.NoError(t, err) + cli.RedirectStderrTo(os.Stderr) + cli.RedirectStdoutTo(os.Stdout) + require.NoError(t, cli.Run()) + } + + // Get the rpc and debug ports + fqbn2, rpcPort, debugPort := getFQBNAndPorts(t) + require.Equal(t, fqbn, fqbn2, "FQBN mismatch between upload and run ports: %s != %s", fqbn, fqbn2) + return rpcPort, debugPort +} + +// getFQBNAndPorts retrieves the FQBN of the board under test and the +// corresponding upload and RPC ports. Debugging messages will be output +// in the upload port, the RPC communication will be done on the RPC port. +// If the board do not have a second serial port, the RPC port will be +// assigned to a USB-2-Serial dongle/converter if found. +func getFQBNAndPorts(t *testing.T) (fqbn string, rpcPort string, uploadPort string) { + cli, err := paths.NewProcess(nil, "arduino-cli", "board", "list", "--json") + require.NoError(t, err) + out, _, err := cli.RunAndCaptureOutput(t.Context()) + require.NoError(t, err) + var cliResult struct { + DetectedPorts []struct { + MatchingBoards []struct { + Fqbn string `json:"fqbn"` + } `json:"matching_boards"` + Port struct { + Address string `json:"address"` + Properties struct { + Vid string `json:"vid"` + Pid string `json:"pid"` + } `json:"properties"` + } `json:"port"` + } `json:"detected_ports"` + } + require.NoError(t, json.Unmarshal(out, &cliResult)) + checkFQBN := func(boardFQBN string) { + if fqbn != boardFQBN { + fqbn = boardFQBN + uploadPort = "" + rpcPort = "" + } + } + for _, port := range cliResult.DetectedPorts { + for _, board := range port.MatchingBoards { + if board.Fqbn == "arduino:mbed_giga:giga" { + checkFQBN(board.Fqbn) + uploadPort = port.Port.Address + } + if board.Fqbn == "arduino:samd:arduino_zero_edbg" { + checkFQBN("arduino:samd:arduino_zero_native") + rpcPort = port.Port.Address + } + if board.Fqbn == "arduino:samd:arduino_zero_native" { + checkFQBN(board.Fqbn) + uploadPort = port.Port.Address + } + if board.Fqbn == "arduino:mbed_nano:nanorp2040connect" { + checkFQBN(board.Fqbn) + uploadPort = port.Port.Address + } + } + } + if rpcPort == "" { + for _, port := range cliResult.DetectedPorts { + if port.Port.Properties.Vid == "0x0483" && port.Port.Properties.Pid == "0x374B" { + rpcPort = port.Port.Address + } + if port.Port.Properties.Vid == "0x1A86" && port.Port.Properties.Pid == "0x55D4" { + rpcPort = port.Port.Address + } + } + } + require.NotEmpty(t, uploadPort, "Upload port not found") + require.NotEmpty(t, rpcPort, "Debug port not found") + return fqbn, rpcPort, uploadPort +} + +// Expect checks that the input stream returns the expected +// string. It reads the input stream until it has read the +// expected number of bytes. It is used to check the output +// of the Arduino board. +func Expect(t *testing.T, in io.Reader, expected string) { + buff := make([]byte, len(expected)) + read := 0 + for read < len(expected) { + n, err := in.Read(buff[read:]) + require.NoError(t, err) + read += n + } + require.Equal(t, expected, string(buff)) +} + +// DebugStream is a wrapper around io.ReadWriteCloser that logs the data +// read and written to the stream in hex format. +// It is used to debug the communication with the Arduino board. +type DebugStream struct { + Upstream io.ReadWriteCloser + Portname string +} + +func (d *DebugStream) Read(p []byte) (n int, err error) { + n, err = d.Upstream.Read(p) + if err != nil { + fmt.Printf("%s READ ERROR: %v\n", d.Portname, err) + } else { + fmt.Printf("%s READ << %s\n", d.Portname, hex.EncodeToString(p[:n])) + } + return n, err +} + +func (d *DebugStream) Write(p []byte) (n int, err error) { + n, err = d.Upstream.Write(p) + if err != nil { + fmt.Printf("%s WRITE ERROR: %v\n", d.Portname, err) + } else { + fmt.Printf("%s WRITE >> %s\n", d.Portname, hex.EncodeToString(p[:n])) + } + return n, err +} + +func (d *DebugStream) Close() error { + err := d.Upstream.Close() + fmt.Printf("%s CLOSE", d.Portname) + if err != nil { + fmt.Printf(" (ERROR: %v)", err) + } + fmt.Println() + return err +}