Skip to content

Commit a827eaf

Browse files
committed
Added integration test
1 parent 482f097 commit a827eaf

File tree

3 files changed

+150
-0
lines changed

3 files changed

+150
-0
lines changed

internal/integrationtest/arduino-cli.go

+19
Original file line numberDiff line numberDiff line change
@@ -602,3 +602,22 @@ func (inst *ArduinoCLIInstance) PlatformSearch(ctx context.Context, args string,
602602
resp, err := inst.cli.daemonClient.PlatformSearch(ctx, req)
603603
return resp, err
604604
}
605+
606+
// Monitor calls the "Monitor" gRPC method and sends the OpenRequest message.
607+
func (inst *ArduinoCLIInstance) Monitor(ctx context.Context, port *commands.Port) (commands.ArduinoCoreService_MonitorClient, error) {
608+
req := &commands.MonitorRequest{}
609+
logCallf(">>> Monitor(%+v)\n", req)
610+
monitorClient, err := inst.cli.daemonClient.Monitor(ctx)
611+
if err != nil {
612+
return nil, err
613+
}
614+
err = monitorClient.Send(&commands.MonitorRequest{
615+
Message: &commands.MonitorRequest_OpenRequest{
616+
OpenRequest: &commands.MonitorPortOpenRequest{
617+
Instance: inst.instance,
618+
Port: port,
619+
},
620+
},
621+
})
622+
return monitorClient, err
623+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// This file is part of arduino-cli.
2+
//
3+
// Copyright 2023 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-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 monitor_test
17+
18+
import (
19+
"context"
20+
"fmt"
21+
"io"
22+
"regexp"
23+
"testing"
24+
"time"
25+
26+
"github.com/arduino/arduino-cli/internal/integrationtest"
27+
"github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1"
28+
"github.com/arduino/go-paths-helper"
29+
"github.com/stretchr/testify/require"
30+
)
31+
32+
func TestMonitorGRPCClose(t *testing.T) {
33+
// See: https://github.com/arduino/arduino-cli/issues/2271
34+
35+
env, cli := integrationtest.CreateEnvForDaemon(t)
36+
defer env.CleanUp()
37+
38+
_, _, err := cli.Run("core", "install", "arduino:[email protected]")
39+
require.NoError(t, err)
40+
41+
cli.InstallMockedSerialDiscovery(t)
42+
cli.InstallMockedSerialMonitor(t)
43+
44+
grpcInst := cli.Create()
45+
require.NoError(t, grpcInst.Init("", "", func(ir *commands.InitResponse) {
46+
fmt.Printf("INIT> %v\n", ir.GetMessage())
47+
}))
48+
49+
// Run a one-shot board list
50+
boardListResp, err := grpcInst.BoardList(time.Second)
51+
require.NoError(t, err)
52+
ports := boardListResp.GetPorts()
53+
require.NotEmpty(t, ports)
54+
fmt.Printf("Got boardlist response with %d ports\n", len(ports))
55+
56+
// Open mocked serial-monitor and close it client-side
57+
tmpFileMatcher := regexp.MustCompile("Tmpfile: (.*)\n")
58+
{
59+
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
60+
mon, err := grpcInst.Monitor(ctx, ports[0].Port)
61+
var tmpFile *paths.Path
62+
for {
63+
monResp, err := mon.Recv()
64+
if err != nil {
65+
fmt.Println("MON>", err)
66+
break
67+
}
68+
fmt.Printf("MON> %v\n", monResp)
69+
if rx := monResp.GetRxData(); rx != nil {
70+
if matches := tmpFileMatcher.FindAllStringSubmatch(string(rx), -1); len(matches) > 0 {
71+
fmt.Println("Found tmpFile", matches[0][1])
72+
tmpFile = paths.New(matches[0][1])
73+
}
74+
}
75+
}
76+
require.NotNil(t, tmpFile)
77+
// The port is close client-side, it may be still open server-side
78+
require.True(t, tmpFile.Exist())
79+
cancel()
80+
require.NoError(t, err)
81+
}
82+
83+
// Now close the monitor using MonitorRequest_Close
84+
for tries := 0; tries < 5; tries++ { // Try the test 5 times to avoid flukes
85+
// Keep a timeout to allow the test to exit in any case
86+
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
87+
mon, err := grpcInst.Monitor(ctx, ports[0].Port)
88+
var tmpFile *paths.Path
89+
for {
90+
monResp, err := mon.Recv()
91+
if err == io.EOF {
92+
fmt.Println("MON>", err)
93+
break
94+
}
95+
96+
require.NoError(t, err)
97+
fmt.Printf("MON> %v\n", monResp)
98+
if rx := monResp.GetRxData(); rx != nil {
99+
if matches := tmpFileMatcher.FindAllStringSubmatch(string(rx), -1); len(matches) > 0 {
100+
fmt.Println("Found tmpFile", matches[0][1])
101+
tmpFile = paths.New(matches[0][1])
102+
go func() {
103+
time.Sleep(time.Second)
104+
fmt.Println("<MON Sent close command")
105+
mon.Send(&commands.MonitorRequest{Message: &commands.MonitorRequest_Close{Close: true}})
106+
}()
107+
}
108+
}
109+
}
110+
require.NotNil(t, tmpFile)
111+
// The port is closed serverd-side, it must be already closed once the client has received the EOF
112+
require.False(t, tmpFile.Exist())
113+
cancel()
114+
require.NoError(t, err)
115+
}
116+
}

internal/mock_serial_monitor/main.go

+15
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ import (
2424
"os"
2525
"slices"
2626
"strings"
27+
"time"
2728

29+
"github.com/arduino/go-paths-helper"
2830
monitor "github.com/arduino/pluggable-monitor-protocol-handler"
2931
)
3032

@@ -41,6 +43,7 @@ type SerialMonitor struct {
4143
mockedSerialPort io.ReadWriteCloser
4244
serialSettings *monitor.PortDescriptor
4345
openedPort bool
46+
muxFile *paths.Path
4447
}
4548

4649
// NewSerialMonitor will initialize and return a SerialMonitor
@@ -129,9 +132,16 @@ func (d *SerialMonitor) Open(boardPort string) (io.ReadWriter, error) {
129132
d.openedPort = true
130133
sideA, sideB := newBidirectionalPipe()
131134
d.mockedSerialPort = sideA
135+
if muxFile, err := paths.MkTempFile(nil, ""); err == nil {
136+
d.muxFile = paths.NewFromFile(muxFile)
137+
muxFile.Close()
138+
}
132139
go func() {
133140
buff := make([]byte, 1024)
134141
d.mockedSerialPort.Write([]byte("Opened port: " + boardPort + "\n"))
142+
if d.muxFile != nil {
143+
d.mockedSerialPort.Write([]byte("Tmpfile: " + d.muxFile.String() + "\n"))
144+
}
135145
for parameter, descriptor := range d.serialSettings.ConfigurationParameter {
136146
d.mockedSerialPort.Write([]byte(
137147
fmt.Sprintf("Configuration %s = %s\n", parameter, descriptor.Selected)))
@@ -186,6 +196,11 @@ func (d *SerialMonitor) Close() error {
186196
}
187197
d.mockedSerialPort.Close()
188198
d.openedPort = false
199+
if d.muxFile != nil {
200+
time.Sleep(500 * time.Millisecond) // Emulate a small delay closing the port to check gRPC synchronization
201+
d.muxFile.Remove()
202+
d.muxFile = nil
203+
}
189204
return nil
190205
}
191206

0 commit comments

Comments
 (0)