Skip to content

Commit

Permalink
[skip-changelog] testsuite: added mocked serial monitor for integrati…
Browse files Browse the repository at this point in the history
…on 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
  • Loading branch information
cmaglie authored Oct 23, 2023
1 parent b82a519 commit eb8f2f2
Show file tree
Hide file tree
Showing 10 changed files with 398 additions and 22 deletions.
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
github.com/arduino/go-timeutils v0.0.0-20171220113728-d1dd9e313b1b
github.com/arduino/go-win32-utils v1.0.0
github.com/arduino/pluggable-discovery-protocol-handler/v2 v2.1.1
github.com/arduino/pluggable-monitor-protocol-handler v0.9.2
github.com/cmaglie/pb v1.0.27
github.com/codeclysm/extract/v3 v3.1.1
github.com/djherbis/buffer v1.2.0
Expand Down Expand Up @@ -60,6 +61,8 @@ require (
github.com/go-git/go-billy/v5 v5.5.0 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/h2non/filetype v1.1.3 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
Expand Down
5 changes: 5 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ github.com/arduino/go-win32-utils v1.0.0 h1:/cXB86sOJxOsCHP7sQmXGLkdValwJt56mIwO
github.com/arduino/go-win32-utils v1.0.0/go.mod h1:0jqM7doGEAs6DaJCxxhLBUDS5OawrqF48HqXkcEie/Q=
github.com/arduino/pluggable-discovery-protocol-handler/v2 v2.1.1 h1:MPQZ2YImq5qBiOPwTFGOrl6E99XGSRHc+UzHA6hsjvc=
github.com/arduino/pluggable-discovery-protocol-handler/v2 v2.1.1/go.mod h1:2lA930B1Xu/otYT1kbx3l1n5vFJuuyPNkQaqOoQHmPE=
github.com/arduino/pluggable-monitor-protocol-handler v0.9.2 h1:vb5AmE3bT9we5Ej4AdBxcC9dJLXasRimVqaComf9L3M=
github.com/arduino/pluggable-monitor-protocol-handler v0.9.2/go.mod h1:vMG8tgHyE+hli26oT0JB/M7NxUMzzWoU5wd6cgJQRK4=
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
Expand Down Expand Up @@ -231,11 +233,14 @@ github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
Expand Down
20 changes: 13 additions & 7 deletions internal/cli/feedback/terminal.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,21 +35,26 @@ func InteractiveStreams() (io.Reader, io.Writer, error) {
if format != Text {
return nil, nil, errors.New(tr("interactive terminal not supported for the '%s' output format", format))
}
if !isTerminal() {
return nil, nil, errors.New(tr("not running in a terminal"))
}
return os.Stdin, stdOut, nil
}

var oldStateStdin *term.State

// SetRawModeStdin sets the stdin stream in RAW mode (no buffering, echo disabled,
// no terminal escape codes nor signals interpreted)
func SetRawModeStdin() {
func SetRawModeStdin() error {
if oldStateStdin != nil {
panic("terminal already in RAW mode")
}
oldStateStdin, _ = term.MakeRaw(int(os.Stdin.Fd()))
if !IsTerminal() {
return errors.New(tr("not running in a terminal"))
}
old, err := term.MakeRaw(int(os.Stdin.Fd()))
if err != nil {
return err
}
oldStateStdin = old
return nil
}

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

func isTerminal() bool {
// IsTerminal returns true if there is an interactive terminal
func IsTerminal() bool {
return term.IsTerminal(int(os.Stdin.Fd()))
}

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

Expand Down
10 changes: 6 additions & 4 deletions internal/cli/monitor/monitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,10 +169,12 @@ func runMonitorCmd(portArgs *arguments.Port, fqbn *arguments.Fqbn, configs []str

ctx, cancel := cleanup.InterruptableContext(context.Background())
if raw {
feedback.SetRawModeStdin()
defer func() {
feedback.RestoreModeStdin()
}()
if feedback.IsTerminal() {
if err := feedback.SetRawModeStdin(); err != nil {
feedback.Warning(tr("Error setting raw mode: %s", err.Error()))
}
defer feedback.RestoreModeStdin()
}

// In RAW mode CTRL-C is not converted into an Interrupt by
// the terminal, we must intercept ASCII 3 (CTRL-C) on our own...
Expand Down
98 changes: 87 additions & 11 deletions internal/integrationtest/arduino-cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"fmt"
"io"
"os"
"runtime"
"strings"
"sync"
"testing"
Expand Down Expand Up @@ -204,32 +205,98 @@ func (cli *ArduinoCLI) convertEnvForExecutils(env map[string]string) []string {
// InstallMockedSerialDiscovery will replace the already installed serial-discovery
// with a mocked one.
func (cli *ArduinoCLI) InstallMockedSerialDiscovery(t *testing.T) {
fmt.Println(color.BlueString("<<< Install mocked serial-discovery"))

// Build mocked serial-discovery
mockDir := FindRepositoryRootPath(t).Join("internal", "integrationtest", "mock_serial_discovery")
mockDir := FindRepositoryRootPath(t).Join("internal", "mock_serial_discovery")
gobuild, err := executils.NewProcess(nil, "go", "build")
require.NoError(t, err)
gobuild.SetDirFromPath(mockDir)
require.NoError(t, gobuild.Run(), "Building mocked serial-discovery")
ext := ""
if runtime.GOOS == "windows" {
ext = ".exe"
}
mockBin := mockDir.Join("mock_serial_discovery" + ext)
require.True(t, mockBin.Exist())
fmt.Println(color.HiBlackString(" Build of mocked serial-discovery succeeded."))

// Install it replacing the current serial discovery
mockBin := mockDir.Join("mock_serial_discovery")
dataDir := cli.DataDir()
require.NotNil(t, dataDir, "data dir missing")
serialDiscoveries, err := dataDir.Join("packages", "builtin", "tools", "serial-discovery").ReadDirRecursiveFiltered(
nil, paths.AndFilter(
paths.FilterNames("serial-discovery"),
paths.FilterNames("serial-discovery"+ext),
paths.FilterOutDirectories(),
),
)
require.NoError(t, err, "scanning data dir for serial-discoveries")
require.NotEmpty(t, serialDiscoveries, "no serial-discoveries found in data dir")
for _, serialDiscovery := range serialDiscoveries {
require.NoError(t, mockBin.CopyTo(serialDiscovery), "installing mocked serial discovery to %s", serialDiscovery)
fmt.Println(color.HiBlackString(" Discovery installed in " + serialDiscovery.String()))
}
}

// InstallMockedSerialMonitor will replace the already installed serial-monitor
// with a mocked one.
func (cli *ArduinoCLI) InstallMockedSerialMonitor(t *testing.T) {
fmt.Println(color.BlueString("<<< Install mocked serial-monitor"))

// Build mocked serial-monitor
mockDir := FindRepositoryRootPath(t).Join("internal", "mock_serial_monitor")
gobuild, err := executils.NewProcess(nil, "go", "build")
require.NoError(t, err)
gobuild.SetDirFromPath(mockDir)
require.NoError(t, gobuild.Run(), "Building mocked serial-monitor")
ext := ""
if runtime.GOOS == "windows" {
ext = ".exe"
}
mockBin := mockDir.Join("mock_serial_monitor" + ext)
require.True(t, mockBin.Exist())
fmt.Println(color.HiBlackString(" Build of mocked serial-monitor succeeded."))

// Install it replacing the current serial monitor
dataDir := cli.DataDir()
require.NotNil(t, dataDir, "data dir missing")
serialMonitors, err := dataDir.Join("packages", "builtin", "tools", "serial-monitor").ReadDirRecursiveFiltered(
nil, paths.AndFilter(
paths.FilterNames("serial-monitor"+ext),
paths.FilterOutDirectories(),
),
)
require.NoError(t, err, "scanning data dir for serial-monitor")
require.NotEmpty(t, serialMonitors, "no serial-monitor found in data dir")
for _, serialMonitor := range serialMonitors {
require.NoError(t, mockBin.CopyTo(serialMonitor), "installing mocked serial monitor to %s", serialMonitor)
fmt.Println(color.HiBlackString(" Monitor installed in " + serialMonitor.String()))
}
}

// RunWithCustomEnv executes the given arduino-cli command with the given custom env and returns the output.
func (cli *ArduinoCLI) RunWithCustomEnv(env map[string]string, args ...string) ([]byte, []byte, error) {
var stdoutBuf, stderrBuf bytes.Buffer
err := cli.run(&stdoutBuf, &stderrBuf, nil, env, args...)

errBuf := stderrBuf.Bytes()
cli.t.NotContains(string(errBuf), "panic: runtime error:", "arduino-cli panicked")

return stdoutBuf.Bytes(), errBuf, err
}

// RunWithCustomInput executes the given arduino-cli command pushing the given input stream and returns the output.
func (cli *ArduinoCLI) RunWithCustomInput(in io.Reader, args ...string) ([]byte, []byte, error) {
var stdoutBuf, stderrBuf bytes.Buffer
err := cli.run(&stdoutBuf, &stderrBuf, in, cli.cliEnvVars, args...)

errBuf := stderrBuf.Bytes()
cli.t.NotContains(string(errBuf), "panic: runtime error:", "arduino-cli panicked")

return stdoutBuf.Bytes(), errBuf, err
}

func (cli *ArduinoCLI) run(stdoutBuff, stderrBuff io.Writer, stdinBuff io.Reader, env map[string]string, args ...string) error {
if cli.cliConfigPath != nil {
args = append([]string{"--config-file", cli.cliConfigPath.String()}, args...)
}
Expand All @@ -240,35 +307,44 @@ func (cli *ArduinoCLI) RunWithCustomEnv(env map[string]string, args ...string) (
cli.t.NoError(err)
stderr, err := cliProc.StderrPipe()
cli.t.NoError(err)
_, err = cliProc.StdinPipe()
stdin, err := cliProc.StdinPipe()
cli.t.NoError(err)
cliProc.SetDir(cli.WorkingDir().String())

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

var stdoutBuf, stderrBuf bytes.Buffer
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
if _, err := io.Copy(&stdoutBuf, io.TeeReader(stdout, os.Stdout)); err != nil {
if stdoutBuff == nil {
stdoutBuff = io.Discard
}
if _, err := io.Copy(stdoutBuff, io.TeeReader(stdout, os.Stdout)); err != nil {
fmt.Println(color.HiBlackString("<<< stdout copy error:"), err)
}
}()
go func() {
defer wg.Done()
if _, err := io.Copy(&stderrBuf, io.TeeReader(stderr, os.Stderr)); err != nil {
if stderrBuff == nil {
stderrBuff = io.Discard
}
if _, err := io.Copy(stderrBuff, io.TeeReader(stderr, os.Stderr)); err != nil {
fmt.Println(color.HiBlackString("<<< stderr copy error:"), err)
}
}()
if stdinBuff != nil {
go func() {
if _, err := io.Copy(stdin, stdinBuff); err != nil {
fmt.Println(color.HiBlackString("<<< stdin copy error:"), err)
}
}()
}
wg.Wait()
cliErr := cliProc.Wait()
fmt.Println(color.HiBlackString("<<< Run completed (err = %v)", cliErr))

errBuf := stderrBuf.Bytes()
cli.t.NotContains(string(errBuf), "panic: runtime error:", "arduino-cli panicked")

return stdoutBuf.Bytes(), errBuf, cliErr
return cliErr
}

// StartDaemon starts the Arduino CLI daemon. It returns the address of the daemon.
Expand Down
90 changes: 90 additions & 0 deletions internal/integrationtest/monitor/monitor_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// This file is part of arduino-cli.
//
// Copyright 2023 ARDUINO SA (http://www.arduino.cc/)
//
// This software is released under the GNU General Public License version 3,
// which covers the main part of arduino-cli.
// The terms of this license can be found at:
// https://www.gnu.org/licenses/gpl-3.0.en.html
//
// You can be released from the requirements of the above licenses by purchasing
// a commercial license. Buying such a license is mandatory if you want to
// modify or otherwise use the software for commercial activities involving the
// Arduino software without disclosing the source code of your own applications.
// To purchase a commercial license, send an email to [email protected].

package monitor_test

import (
"bytes"
"io"
"testing"

"github.com/arduino/arduino-cli/internal/integrationtest"
"github.com/stretchr/testify/require"
)

func TestMonitorConfigFlags(t *testing.T) {
env, cli := integrationtest.CreateArduinoCLIWithEnvironment(t)
defer env.CleanUp()

// Install AVR platform (this is required to enable the 'serial' monitor...)
// TODO: maybe this is worth opening an issue?
_, _, err := cli.Run("core", "install", "arduino:[email protected]")
require.NoError(t, err)

// Install mocked discovery and monitor for testing
require.NoError(t, err)
cli.InstallMockedSerialDiscovery(t)
cli.InstallMockedSerialMonitor(t)

// Test monitor command
quit := func() io.Reader {
// tells mocked monitor to exit
return bytes.NewBufferString("QUIT\n")
}

t.Run("NoArgs", func(t *testing.T) {
stdout, _, err := cli.RunWithCustomInput(quit(), "monitor", "-p", "/dev/ttyARG", "--raw")
require.NoError(t, err)
require.Contains(t, string(stdout), "Opened port: /dev/ttyARG")
require.Contains(t, string(stdout), "Configuration baudrate = 9600")
require.Contains(t, string(stdout), "Configuration rts = on")
require.Contains(t, string(stdout), "Configuration dtr = on")
})

t.Run("BaudConfig", func(t *testing.T) {
stdout, _, err := cli.RunWithCustomInput(quit(), "monitor", "-p", "/dev/ttyARG", "-c", "baudrate=115200", "--raw")
require.NoError(t, err)
require.Contains(t, string(stdout), "Opened port: /dev/ttyARG")
require.Contains(t, string(stdout), "Configuration baudrate = 115200")
require.Contains(t, string(stdout), "Configuration parity = none")
require.Contains(t, string(stdout), "Configuration rts = on")
require.Contains(t, string(stdout), "Configuration dtr = on")
})

t.Run("BaudAndParitfyConfig", func(t *testing.T) {
stdout, _, err := cli.RunWithCustomInput(quit(), "monitor", "-p", "/dev/ttyARG",
"-c", "baudrate=115200", "-c", "parity=even", "--raw")
require.NoError(t, err)
require.Contains(t, string(stdout), "Opened port: /dev/ttyARG")
require.Contains(t, string(stdout), "Configuration baudrate = 115200")
require.Contains(t, string(stdout), "Configuration parity = even")
require.Contains(t, string(stdout), "Configuration rts = on")
require.Contains(t, string(stdout), "Configuration dtr = on")
})

t.Run("InvalidConfigKey", func(t *testing.T) {
_, stderr, err := cli.RunWithCustomInput(quit(), "monitor", "-p", "/dev/ttyARG",
"-c", "baud=115200", "-c", "parity=even", "--raw")
require.Error(t, err)
require.Contains(t, string(stderr), "invalid port configuration: baud=115200")
})

t.Run("InvalidConfigValue", func(t *testing.T) {
_, stderr, err := cli.RunWithCustomInput(quit(), "monitor", "-p", "/dev/ttyARG",
"-c", "parity=9600", "--raw")
require.Error(t, err)
require.Contains(t, string(stderr), "invalid port configuration value for parity: 9600")
})
}
File renamed without changes.
File renamed without changes.
1 change: 1 addition & 0 deletions internal/mock_serial_monitor/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
mock_serial_monitor
Loading

0 comments on commit eb8f2f2

Please sign in to comment.