From eb8f2f246d3dc3afabe276d35b589ed732898835 Mon Sep 17 00:00:00 2001 From: Cristian Maglie Date: Mon, 23 Oct 2023 11:09:03 +0200 Subject: [PATCH] [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 --- go.mod | 3 + go.sum | 5 + internal/cli/feedback/terminal.go | 20 +- internal/cli/monitor/monitor.go | 10 +- internal/integrationtest/arduino-cli.go | 98 ++++++++- .../integrationtest/monitor/monitor_test.go | 90 ++++++++ .../mock_serial_discovery/.gitignore | 0 .../mock_serial_discovery/main.go | 0 internal/mock_serial_monitor/.gitignore | 1 + internal/mock_serial_monitor/main.go | 193 ++++++++++++++++++ 10 files changed, 398 insertions(+), 22 deletions(-) create mode 100644 internal/integrationtest/monitor/monitor_test.go rename internal/{integrationtest => }/mock_serial_discovery/.gitignore (100%) rename internal/{integrationtest => }/mock_serial_discovery/main.go (100%) create mode 100644 internal/mock_serial_monitor/.gitignore create mode 100644 internal/mock_serial_monitor/main.go diff --git a/go.mod b/go.mod index 3ff632d87f4..86bc603bf72 100644 --- a/go.mod +++ b/go.mod @@ -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 @@ -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 diff --git a/go.sum b/go.sum index ff74254e44e..b2674d86777 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -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= diff --git a/internal/cli/feedback/terminal.go b/internal/cli/feedback/terminal.go index 11c466bae8b..09dc0212f4a 100644 --- a/internal/cli/feedback/terminal.go +++ b/internal/cli/feedback/terminal.go @@ -35,9 +35,6 @@ 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 } @@ -45,11 +42,19 @@ 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 @@ -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())) } @@ -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")) } diff --git a/internal/cli/monitor/monitor.go b/internal/cli/monitor/monitor.go index b15cd5bb436..c7cdac4810a 100644 --- a/internal/cli/monitor/monitor.go +++ b/internal/cli/monitor/monitor.go @@ -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... diff --git a/internal/integrationtest/arduino-cli.go b/internal/integrationtest/arduino-cli.go index dd1855577aa..087182c50af 100644 --- a/internal/integrationtest/arduino-cli.go +++ b/internal/integrationtest/arduino-cli.go @@ -23,6 +23,7 @@ import ( "fmt" "io" "os" + "runtime" "strings" "sync" "testing" @@ -204,20 +205,28 @@ 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(), ), ) @@ -225,11 +234,69 @@ func (cli *ArduinoCLI) InstallMockedSerialDiscovery(t *testing.T) { 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...) } @@ -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. diff --git a/internal/integrationtest/monitor/monitor_test.go b/internal/integrationtest/monitor/monitor_test.go new file mode 100644 index 00000000000..0f5593cb9aa --- /dev/null +++ b/internal/integrationtest/monitor/monitor_test.go @@ -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 license@arduino.cc. + +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:avr@1.8.6") + 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") + }) +} diff --git a/internal/integrationtest/mock_serial_discovery/.gitignore b/internal/mock_serial_discovery/.gitignore similarity index 100% rename from internal/integrationtest/mock_serial_discovery/.gitignore rename to internal/mock_serial_discovery/.gitignore diff --git a/internal/integrationtest/mock_serial_discovery/main.go b/internal/mock_serial_discovery/main.go similarity index 100% rename from internal/integrationtest/mock_serial_discovery/main.go rename to internal/mock_serial_discovery/main.go diff --git a/internal/mock_serial_monitor/.gitignore b/internal/mock_serial_monitor/.gitignore new file mode 100644 index 00000000000..eae1d555aab --- /dev/null +++ b/internal/mock_serial_monitor/.gitignore @@ -0,0 +1 @@ +mock_serial_monitor diff --git a/internal/mock_serial_monitor/main.go b/internal/mock_serial_monitor/main.go new file mode 100644 index 00000000000..4dc9c4da159 --- /dev/null +++ b/internal/mock_serial_monitor/main.go @@ -0,0 +1,193 @@ +// +// 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 license@arduino.cc. +// + +package main + +import ( + "errors" + "fmt" + "io" + "os" + "slices" + "strings" + + monitor "github.com/arduino/pluggable-monitor-protocol-handler" +) + +func main() { + monitorServer := monitor.NewServer(NewSerialMonitor()) + if err := monitorServer.Run(os.Stdin, os.Stdout); err != nil { + fmt.Fprintf(os.Stderr, "Error: %s\n", err.Error()) + os.Exit(1) + } +} + +// SerialMonitor is the implementation of the serial ports pluggable-monitor +type SerialMonitor struct { + mockedSerialPort io.ReadWriteCloser + serialSettings *monitor.PortDescriptor + openedPort bool +} + +// NewSerialMonitor will initialize and return a SerialMonitor +func NewSerialMonitor() *SerialMonitor { + return &SerialMonitor{ + serialSettings: &monitor.PortDescriptor{ + Protocol: "serial", + ConfigurationParameter: map[string]*monitor.PortParameterDescriptor{ + "baudrate": { + Label: "Baudrate", + Type: "enum", + Values: []string{ + "300", "600", "750", + "1200", "2400", "4800", "9600", + "19200", "31250", "38400", "57600", "74880", + "115200", "230400", "250000", "460800", "500000", "921600", + "1000000", "2000000"}, + Selected: "9600", + }, + "parity": { + Label: "Parity", + Type: "enum", + Values: []string{"none", "even", "odd", "mark", "space"}, + Selected: "none", + }, + "bits": { + Label: "Data bits", + Type: "enum", + Values: []string{"5", "6", "7", "8", "9"}, + Selected: "8", + }, + "stop_bits": { + Label: "Stop bits", + Type: "enum", + Values: []string{"1", "1.5", "2"}, + Selected: "1", + }, + "rts": { + Label: "RTS", + Type: "enum", + Values: []string{"on", "off"}, + Selected: "on", + }, + "dtr": { + Label: "DTR", + Type: "enum", + Values: []string{"on", "off"}, + Selected: "on", + }, + }, + }, + openedPort: false, + } +} + +// Hello is the handler for the pluggable-monitor HELLO command +func (d *SerialMonitor) Hello(userAgent string, protocol int) error { + return nil +} + +// Describe is the handler for the pluggable-monitor DESCRIBE command +func (d *SerialMonitor) Describe() (*monitor.PortDescriptor, error) { + return d.serialSettings, nil +} + +// Configure is the handler for the pluggable-monitor CONFIGURE command +func (d *SerialMonitor) Configure(parameterName string, value string) error { + parameter, ok := d.serialSettings.ConfigurationParameter[parameterName] + if !ok { + return fmt.Errorf("could not find parameter named %s", parameterName) + } + if !slices.Contains(parameter.Values, value) { + return fmt.Errorf("invalid value for parameter %s: %s", parameterName, value) + } + // Set configuration + parameter.Selected = value + + return nil +} + +// Open is the handler for the pluggable-monitor OPEN command +func (d *SerialMonitor) Open(boardPort string) (io.ReadWriter, error) { + if d.openedPort { + return nil, fmt.Errorf("port already opened: %s", boardPort) + } + d.openedPort = true + sideA, sideB := newBidirectionalPipe() + d.mockedSerialPort = sideA + go func() { + buff := make([]byte, 1024) + d.mockedSerialPort.Write([]byte("Opened port: " + boardPort + "\n")) + for parameter, descriptor := range d.serialSettings.ConfigurationParameter { + d.mockedSerialPort.Write([]byte( + fmt.Sprintf("Configuration %s = %s\n", parameter, descriptor.Selected))) + } + for { + n, err := d.mockedSerialPort.Read(buff) + if err != nil { + d.mockedSerialPort.Close() + return + } + if strings.Contains(string(buff[:n]), "QUIT") { + d.mockedSerialPort.Close() + return + } + d.mockedSerialPort.Write([]byte("Received: >")) + d.mockedSerialPort.Write(buff[:n]) + d.mockedSerialPort.Write([]byte("<\n")) + } + }() + return sideB, nil +} + +func newBidirectionalPipe() (io.ReadWriteCloser, io.ReadWriteCloser) { + in1, out1 := io.Pipe() + in2, out2 := io.Pipe() + a := &bidirectionalPipe{in: in1, out: out2} + b := &bidirectionalPipe{in: in2, out: out1} + return a, b +} + +type bidirectionalPipe struct { + in io.Reader + out io.WriteCloser +} + +func (p *bidirectionalPipe) Read(buff []byte) (int, error) { + return p.in.Read(buff) +} + +func (p *bidirectionalPipe) Write(buff []byte) (int, error) { + return p.out.Write(buff) +} + +func (p *bidirectionalPipe) Close() error { + return p.out.Close() +} + +// Close is the handler for the pluggable-monitor CLOSE command +func (d *SerialMonitor) Close() error { + if !d.openedPort { + return errors.New("port already closed") + } + d.mockedSerialPort.Close() + d.openedPort = false + return nil +} + +// Quit is the handler for the pluggable-monitor QUIT command +func (d *SerialMonitor) Quit() {}