Skip to content

Commit eb8f2f2

Browse files
authored
[skip-changelog] testsuite: added mocked serial monitor for integration tests (#2379)
* Added mocked serial-monitor for integration tests * Give a bit more output when using mocking tests * Allow testsuite to run arduino-cli with a given input stream * Allow use of the monitor in non-terminal envs * Added monitor command integration tests * Moved mocked tools packages up one dir to not get 'executed' as integration test * Consider .exe extension on Windows when implating mocked tools
1 parent b82a519 commit eb8f2f2

File tree

10 files changed

+398
-22
lines changed

10 files changed

+398
-22
lines changed

Diff for: go.mod

+3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ require (
1212
github.com/arduino/go-timeutils v0.0.0-20171220113728-d1dd9e313b1b
1313
github.com/arduino/go-win32-utils v1.0.0
1414
github.com/arduino/pluggable-discovery-protocol-handler/v2 v2.1.1
15+
github.com/arduino/pluggable-monitor-protocol-handler v0.9.2
1516
github.com/cmaglie/pb v1.0.27
1617
github.com/codeclysm/extract/v3 v3.1.1
1718
github.com/djherbis/buffer v1.2.0
@@ -60,6 +61,8 @@ require (
6061
github.com/go-git/go-billy/v5 v5.5.0 // indirect
6162
github.com/golang/protobuf v1.5.3 // indirect
6263
github.com/h2non/filetype v1.1.3 // indirect
64+
github.com/hashicorp/errwrap v1.0.0 // indirect
65+
github.com/hashicorp/go-multierror v1.1.1 // indirect
6366
github.com/hashicorp/hcl v1.0.0 // indirect
6467
github.com/imdario/mergo v0.3.12 // indirect
6568
github.com/inconshreveable/mousetrap v1.1.0 // indirect

Diff for: go.sum

+5
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ github.com/arduino/go-win32-utils v1.0.0 h1:/cXB86sOJxOsCHP7sQmXGLkdValwJt56mIwO
7272
github.com/arduino/go-win32-utils v1.0.0/go.mod h1:0jqM7doGEAs6DaJCxxhLBUDS5OawrqF48HqXkcEie/Q=
7373
github.com/arduino/pluggable-discovery-protocol-handler/v2 v2.1.1 h1:MPQZ2YImq5qBiOPwTFGOrl6E99XGSRHc+UzHA6hsjvc=
7474
github.com/arduino/pluggable-discovery-protocol-handler/v2 v2.1.1/go.mod h1:2lA930B1Xu/otYT1kbx3l1n5vFJuuyPNkQaqOoQHmPE=
75+
github.com/arduino/pluggable-monitor-protocol-handler v0.9.2 h1:vb5AmE3bT9we5Ej4AdBxcC9dJLXasRimVqaComf9L3M=
76+
github.com/arduino/pluggable-monitor-protocol-handler v0.9.2/go.mod h1:vMG8tgHyE+hli26oT0JB/M7NxUMzzWoU5wd6cgJQRK4=
7577
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
7678
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
7779
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
@@ -231,11 +233,14 @@ github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
231233
github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
232234
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
233235
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
236+
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
234237
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
235238
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
236239
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
237240
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
238241
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
242+
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
243+
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
239244
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
240245
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
241246
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=

Diff for: internal/cli/feedback/terminal.go

+13-7
Original file line numberDiff line numberDiff line change
@@ -35,21 +35,26 @@ func InteractiveStreams() (io.Reader, io.Writer, error) {
3535
if format != Text {
3636
return nil, nil, errors.New(tr("interactive terminal not supported for the '%s' output format", format))
3737
}
38-
if !isTerminal() {
39-
return nil, nil, errors.New(tr("not running in a terminal"))
40-
}
4138
return os.Stdin, stdOut, nil
4239
}
4340

4441
var oldStateStdin *term.State
4542

4643
// SetRawModeStdin sets the stdin stream in RAW mode (no buffering, echo disabled,
4744
// no terminal escape codes nor signals interpreted)
48-
func SetRawModeStdin() {
45+
func SetRawModeStdin() error {
4946
if oldStateStdin != nil {
5047
panic("terminal already in RAW mode")
5148
}
52-
oldStateStdin, _ = term.MakeRaw(int(os.Stdin.Fd()))
49+
if !IsTerminal() {
50+
return errors.New(tr("not running in a terminal"))
51+
}
52+
old, err := term.MakeRaw(int(os.Stdin.Fd()))
53+
if err != nil {
54+
return err
55+
}
56+
oldStateStdin = old
57+
return nil
5358
}
5459

5560
// RestoreModeStdin restore the terminal settings to the normal non-RAW state. This
@@ -63,7 +68,8 @@ func RestoreModeStdin() {
6368
oldStateStdin = nil
6469
}
6570

66-
func isTerminal() bool {
71+
// IsTerminal returns true if there is an interactive terminal
72+
func IsTerminal() bool {
6773
return term.IsTerminal(int(os.Stdin.Fd()))
6874
}
6975

@@ -72,7 +78,7 @@ func InputUserField(prompt string, secret bool) (string, error) {
7278
if format != Text {
7379
return "", errors.New(tr("user input not supported for the '%s' output format", format))
7480
}
75-
if !isTerminal() {
81+
if !IsTerminal() {
7682
return "", errors.New(tr("user input not supported in non interactive mode"))
7783
}
7884

Diff for: internal/cli/monitor/monitor.go

+6-4
Original file line numberDiff line numberDiff line change
@@ -169,10 +169,12 @@ func runMonitorCmd(portArgs *arguments.Port, fqbn *arguments.Fqbn, configs []str
169169

170170
ctx, cancel := cleanup.InterruptableContext(context.Background())
171171
if raw {
172-
feedback.SetRawModeStdin()
173-
defer func() {
174-
feedback.RestoreModeStdin()
175-
}()
172+
if feedback.IsTerminal() {
173+
if err := feedback.SetRawModeStdin(); err != nil {
174+
feedback.Warning(tr("Error setting raw mode: %s", err.Error()))
175+
}
176+
defer feedback.RestoreModeStdin()
177+
}
176178

177179
// In RAW mode CTRL-C is not converted into an Interrupt by
178180
// the terminal, we must intercept ASCII 3 (CTRL-C) on our own...

Diff for: internal/integrationtest/arduino-cli.go

+87-11
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"fmt"
2424
"io"
2525
"os"
26+
"runtime"
2627
"strings"
2728
"sync"
2829
"testing"
@@ -204,32 +205,98 @@ func (cli *ArduinoCLI) convertEnvForExecutils(env map[string]string) []string {
204205
// InstallMockedSerialDiscovery will replace the already installed serial-discovery
205206
// with a mocked one.
206207
func (cli *ArduinoCLI) InstallMockedSerialDiscovery(t *testing.T) {
208+
fmt.Println(color.BlueString("<<< Install mocked serial-discovery"))
209+
207210
// Build mocked serial-discovery
208-
mockDir := FindRepositoryRootPath(t).Join("internal", "integrationtest", "mock_serial_discovery")
211+
mockDir := FindRepositoryRootPath(t).Join("internal", "mock_serial_discovery")
209212
gobuild, err := executils.NewProcess(nil, "go", "build")
210213
require.NoError(t, err)
211214
gobuild.SetDirFromPath(mockDir)
212215
require.NoError(t, gobuild.Run(), "Building mocked serial-discovery")
216+
ext := ""
217+
if runtime.GOOS == "windows" {
218+
ext = ".exe"
219+
}
220+
mockBin := mockDir.Join("mock_serial_discovery" + ext)
221+
require.True(t, mockBin.Exist())
222+
fmt.Println(color.HiBlackString(" Build of mocked serial-discovery succeeded."))
213223

214224
// Install it replacing the current serial discovery
215-
mockBin := mockDir.Join("mock_serial_discovery")
216225
dataDir := cli.DataDir()
217226
require.NotNil(t, dataDir, "data dir missing")
218227
serialDiscoveries, err := dataDir.Join("packages", "builtin", "tools", "serial-discovery").ReadDirRecursiveFiltered(
219228
nil, paths.AndFilter(
220-
paths.FilterNames("serial-discovery"),
229+
paths.FilterNames("serial-discovery"+ext),
221230
paths.FilterOutDirectories(),
222231
),
223232
)
224233
require.NoError(t, err, "scanning data dir for serial-discoveries")
225234
require.NotEmpty(t, serialDiscoveries, "no serial-discoveries found in data dir")
226235
for _, serialDiscovery := range serialDiscoveries {
227236
require.NoError(t, mockBin.CopyTo(serialDiscovery), "installing mocked serial discovery to %s", serialDiscovery)
237+
fmt.Println(color.HiBlackString(" Discovery installed in " + serialDiscovery.String()))
238+
}
239+
}
240+
241+
// InstallMockedSerialMonitor will replace the already installed serial-monitor
242+
// with a mocked one.
243+
func (cli *ArduinoCLI) InstallMockedSerialMonitor(t *testing.T) {
244+
fmt.Println(color.BlueString("<<< Install mocked serial-monitor"))
245+
246+
// Build mocked serial-monitor
247+
mockDir := FindRepositoryRootPath(t).Join("internal", "mock_serial_monitor")
248+
gobuild, err := executils.NewProcess(nil, "go", "build")
249+
require.NoError(t, err)
250+
gobuild.SetDirFromPath(mockDir)
251+
require.NoError(t, gobuild.Run(), "Building mocked serial-monitor")
252+
ext := ""
253+
if runtime.GOOS == "windows" {
254+
ext = ".exe"
255+
}
256+
mockBin := mockDir.Join("mock_serial_monitor" + ext)
257+
require.True(t, mockBin.Exist())
258+
fmt.Println(color.HiBlackString(" Build of mocked serial-monitor succeeded."))
259+
260+
// Install it replacing the current serial monitor
261+
dataDir := cli.DataDir()
262+
require.NotNil(t, dataDir, "data dir missing")
263+
serialMonitors, err := dataDir.Join("packages", "builtin", "tools", "serial-monitor").ReadDirRecursiveFiltered(
264+
nil, paths.AndFilter(
265+
paths.FilterNames("serial-monitor"+ext),
266+
paths.FilterOutDirectories(),
267+
),
268+
)
269+
require.NoError(t, err, "scanning data dir for serial-monitor")
270+
require.NotEmpty(t, serialMonitors, "no serial-monitor found in data dir")
271+
for _, serialMonitor := range serialMonitors {
272+
require.NoError(t, mockBin.CopyTo(serialMonitor), "installing mocked serial monitor to %s", serialMonitor)
273+
fmt.Println(color.HiBlackString(" Monitor installed in " + serialMonitor.String()))
228274
}
229275
}
230276

231277
// RunWithCustomEnv executes the given arduino-cli command with the given custom env and returns the output.
232278
func (cli *ArduinoCLI) RunWithCustomEnv(env map[string]string, args ...string) ([]byte, []byte, error) {
279+
var stdoutBuf, stderrBuf bytes.Buffer
280+
err := cli.run(&stdoutBuf, &stderrBuf, nil, env, args...)
281+
282+
errBuf := stderrBuf.Bytes()
283+
cli.t.NotContains(string(errBuf), "panic: runtime error:", "arduino-cli panicked")
284+
285+
return stdoutBuf.Bytes(), errBuf, err
286+
}
287+
288+
// RunWithCustomInput executes the given arduino-cli command pushing the given input stream and returns the output.
289+
func (cli *ArduinoCLI) RunWithCustomInput(in io.Reader, args ...string) ([]byte, []byte, error) {
290+
var stdoutBuf, stderrBuf bytes.Buffer
291+
err := cli.run(&stdoutBuf, &stderrBuf, in, cli.cliEnvVars, args...)
292+
293+
errBuf := stderrBuf.Bytes()
294+
cli.t.NotContains(string(errBuf), "panic: runtime error:", "arduino-cli panicked")
295+
296+
return stdoutBuf.Bytes(), errBuf, err
297+
}
298+
299+
func (cli *ArduinoCLI) run(stdoutBuff, stderrBuff io.Writer, stdinBuff io.Reader, env map[string]string, args ...string) error {
233300
if cli.cliConfigPath != nil {
234301
args = append([]string{"--config-file", cli.cliConfigPath.String()}, args...)
235302
}
@@ -240,35 +307,44 @@ func (cli *ArduinoCLI) RunWithCustomEnv(env map[string]string, args ...string) (
240307
cli.t.NoError(err)
241308
stderr, err := cliProc.StderrPipe()
242309
cli.t.NoError(err)
243-
_, err = cliProc.StdinPipe()
310+
stdin, err := cliProc.StdinPipe()
244311
cli.t.NoError(err)
245312
cliProc.SetDir(cli.WorkingDir().String())
246313

247314
cli.t.NoError(cliProc.Start())
248315

249-
var stdoutBuf, stderrBuf bytes.Buffer
250316
var wg sync.WaitGroup
251317
wg.Add(2)
252318
go func() {
253319
defer wg.Done()
254-
if _, err := io.Copy(&stdoutBuf, io.TeeReader(stdout, os.Stdout)); err != nil {
320+
if stdoutBuff == nil {
321+
stdoutBuff = io.Discard
322+
}
323+
if _, err := io.Copy(stdoutBuff, io.TeeReader(stdout, os.Stdout)); err != nil {
255324
fmt.Println(color.HiBlackString("<<< stdout copy error:"), err)
256325
}
257326
}()
258327
go func() {
259328
defer wg.Done()
260-
if _, err := io.Copy(&stderrBuf, io.TeeReader(stderr, os.Stderr)); err != nil {
329+
if stderrBuff == nil {
330+
stderrBuff = io.Discard
331+
}
332+
if _, err := io.Copy(stderrBuff, io.TeeReader(stderr, os.Stderr)); err != nil {
261333
fmt.Println(color.HiBlackString("<<< stderr copy error:"), err)
262334
}
263335
}()
336+
if stdinBuff != nil {
337+
go func() {
338+
if _, err := io.Copy(stdin, stdinBuff); err != nil {
339+
fmt.Println(color.HiBlackString("<<< stdin copy error:"), err)
340+
}
341+
}()
342+
}
264343
wg.Wait()
265344
cliErr := cliProc.Wait()
266345
fmt.Println(color.HiBlackString("<<< Run completed (err = %v)", cliErr))
267346

268-
errBuf := stderrBuf.Bytes()
269-
cli.t.NotContains(string(errBuf), "panic: runtime error:", "arduino-cli panicked")
270-
271-
return stdoutBuf.Bytes(), errBuf, cliErr
347+
return cliErr
272348
}
273349

274350
// StartDaemon starts the Arduino CLI daemon. It returns the address of the daemon.

Diff for: internal/integrationtest/monitor/monitor_test.go

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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+
"bytes"
20+
"io"
21+
"testing"
22+
23+
"github.com/arduino/arduino-cli/internal/integrationtest"
24+
"github.com/stretchr/testify/require"
25+
)
26+
27+
func TestMonitorConfigFlags(t *testing.T) {
28+
env, cli := integrationtest.CreateArduinoCLIWithEnvironment(t)
29+
defer env.CleanUp()
30+
31+
// Install AVR platform (this is required to enable the 'serial' monitor...)
32+
// TODO: maybe this is worth opening an issue?
33+
_, _, err := cli.Run("core", "install", "arduino:[email protected]")
34+
require.NoError(t, err)
35+
36+
// Install mocked discovery and monitor for testing
37+
require.NoError(t, err)
38+
cli.InstallMockedSerialDiscovery(t)
39+
cli.InstallMockedSerialMonitor(t)
40+
41+
// Test monitor command
42+
quit := func() io.Reader {
43+
// tells mocked monitor to exit
44+
return bytes.NewBufferString("QUIT\n")
45+
}
46+
47+
t.Run("NoArgs", func(t *testing.T) {
48+
stdout, _, err := cli.RunWithCustomInput(quit(), "monitor", "-p", "/dev/ttyARG", "--raw")
49+
require.NoError(t, err)
50+
require.Contains(t, string(stdout), "Opened port: /dev/ttyARG")
51+
require.Contains(t, string(stdout), "Configuration baudrate = 9600")
52+
require.Contains(t, string(stdout), "Configuration rts = on")
53+
require.Contains(t, string(stdout), "Configuration dtr = on")
54+
})
55+
56+
t.Run("BaudConfig", func(t *testing.T) {
57+
stdout, _, err := cli.RunWithCustomInput(quit(), "monitor", "-p", "/dev/ttyARG", "-c", "baudrate=115200", "--raw")
58+
require.NoError(t, err)
59+
require.Contains(t, string(stdout), "Opened port: /dev/ttyARG")
60+
require.Contains(t, string(stdout), "Configuration baudrate = 115200")
61+
require.Contains(t, string(stdout), "Configuration parity = none")
62+
require.Contains(t, string(stdout), "Configuration rts = on")
63+
require.Contains(t, string(stdout), "Configuration dtr = on")
64+
})
65+
66+
t.Run("BaudAndParitfyConfig", func(t *testing.T) {
67+
stdout, _, err := cli.RunWithCustomInput(quit(), "monitor", "-p", "/dev/ttyARG",
68+
"-c", "baudrate=115200", "-c", "parity=even", "--raw")
69+
require.NoError(t, err)
70+
require.Contains(t, string(stdout), "Opened port: /dev/ttyARG")
71+
require.Contains(t, string(stdout), "Configuration baudrate = 115200")
72+
require.Contains(t, string(stdout), "Configuration parity = even")
73+
require.Contains(t, string(stdout), "Configuration rts = on")
74+
require.Contains(t, string(stdout), "Configuration dtr = on")
75+
})
76+
77+
t.Run("InvalidConfigKey", func(t *testing.T) {
78+
_, stderr, err := cli.RunWithCustomInput(quit(), "monitor", "-p", "/dev/ttyARG",
79+
"-c", "baud=115200", "-c", "parity=even", "--raw")
80+
require.Error(t, err)
81+
require.Contains(t, string(stderr), "invalid port configuration: baud=115200")
82+
})
83+
84+
t.Run("InvalidConfigValue", func(t *testing.T) {
85+
_, stderr, err := cli.RunWithCustomInput(quit(), "monitor", "-p", "/dev/ttyARG",
86+
"-c", "parity=9600", "--raw")
87+
require.Error(t, err)
88+
require.Contains(t, string(stderr), "invalid port configuration value for parity: 9600")
89+
})
90+
}

Diff for: internal/mock_serial_monitor/.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
mock_serial_monitor

0 commit comments

Comments
 (0)