diff --git a/internal/integrationtest/arduino-cli.go b/internal/integrationtest/arduino-cli.go index 62eaa27ee08..231065843d4 100644 --- a/internal/integrationtest/arduino-cli.go +++ b/internal/integrationtest/arduino-cli.go @@ -286,6 +286,42 @@ func (cli *ArduinoCLI) InstallMockedSerialMonitor(t *testing.T) { } } +// InstallMockedAvrdude will replace the already installed avrdude with a mocked one. +func (cli *ArduinoCLI) InstallMockedAvrdude(t *testing.T) { + fmt.Println(color.BlueString("<<< Install mocked avrdude")) + + // Build mocked serial-discovery + mockDir := FindRepositoryRootPath(t).Join("internal", "mock_avrdude") + gobuild, err := paths.NewProcess(nil, "go", "build") + require.NoError(t, err) + gobuild.SetDirFromPath(mockDir) + require.NoError(t, gobuild.Run(), "Building mocked avrdude") + ext := "" + if runtime.GOOS == "windows" { + ext = ".exe" + } + mockBin := mockDir.Join("mock_avrdude" + ext) + require.True(t, mockBin.Exist()) + fmt.Println(color.HiBlackString(" Build of mocked avrdude succeeded.")) + + // Install it replacing the current avrdudes + dataDir := cli.DataDir() + require.NotNil(t, dataDir, "data dir missing") + + avrdudes, err := dataDir.Join("packages", "arduino", "tools", "avrdude").ReadDirRecursiveFiltered( + nil, paths.AndFilter( + paths.FilterNames("avrdude"+ext), + paths.FilterOutDirectories(), + ), + ) + require.NoError(t, err, "scanning data dir for avrdude(s)") + require.NotEmpty(t, avrdudes, "no avrdude(s) found in data dir") + for _, avrdude := range avrdudes { + require.NoError(t, mockBin.CopyTo(avrdude), "installing mocked avrdude to %s", avrdude) + fmt.Println(color.HiBlackString(" Mocked avrdude installed in " + avrdude.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 @@ -642,3 +678,19 @@ func (inst *ArduinoCLIInstance) Monitor(ctx context.Context, port *commands.Port }) return monitorClient, err } + +// Upload calls the "Upload" gRPC method. +func (inst *ArduinoCLIInstance) Upload(ctx context.Context, fqbn, sketchPath, port, protocol string) (commands.ArduinoCoreService_UploadClient, error) { + uploadCl, err := inst.cli.daemonClient.Upload(ctx, &commands.UploadRequest{ + Instance: inst.instance, + Fqbn: fqbn, + SketchPath: sketchPath, + Verbose: true, + Port: &commands.Port{ + Address: port, + Protocol: protocol, + }, + }) + logCallf(">>> Upload(%v %v port/protocol=%s/%s)\n", fqbn, sketchPath, port, protocol) + return uploadCl, err +} diff --git a/internal/integrationtest/daemon/upload_test.go b/internal/integrationtest/daemon/upload_test.go new file mode 100644 index 00000000000..00a85e57479 --- /dev/null +++ b/internal/integrationtest/daemon/upload_test.go @@ -0,0 +1,120 @@ +// This file is part of arduino-cli. +// +// Copyright 2024 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 daemon_test + +import ( + "context" + "errors" + "fmt" + "io" + "os" + "strings" + "testing" + "time" + + "github.com/arduino/arduino-cli/internal/integrationtest" + "github.com/arduino/arduino-cli/rpc/cc/arduino/cli/commands/v1" + "github.com/arduino/go-paths-helper" + "github.com/stretchr/testify/require" +) + +func TestUploadCancelation(t *testing.T) { + env, cli := integrationtest.CreateEnvForDaemon(t) + defer env.CleanUp() + + grpcInst := cli.Create() + require.NoError(t, grpcInst.Init("", "", func(ir *commands.InitResponse) { + fmt.Printf("INIT> %v\n", ir.GetMessage()) + })) + + plInst, err := grpcInst.PlatformInstall(context.Background(), "arduino", "avr", "1.8.6", true) + require.NoError(t, err) + for { + msg, err := plInst.Recv() + if errors.Is(err, io.EOF) { + break + } + require.NoError(t, err) + fmt.Printf("INSTALL> %v\n", msg) + } + + // Mock avrdude + cli.InstallMockedAvrdude(t) + + // Re-init instance to update changes + require.NoError(t, grpcInst.Init("", "", func(ir *commands.InitResponse) { + fmt.Printf("INIT> %v\n", ir.GetMessage()) + })) + + // Build sketch for upload + sk := paths.New("testdata", "bare_minimum") + compile, err := grpcInst.Compile(context.Background(), "arduino:avr:uno", sk.String(), "") + require.NoError(t, err) + for { + msg, err := compile.Recv() + if errors.Is(err, io.EOF) { + break + } + if err != nil { + fmt.Println("COMPILE ERROR>", err) + require.FailNow(t, "Expected successful compile", "compilation failed") + break + } + if msg.GetOutStream() != nil { + fmt.Printf("COMPILE OUT> %v\n", string(msg.GetOutStream())) + } + if msg.GetErrStream() != nil { + fmt.Printf("COMPILE ERR> %v\n", string(msg.GetErrStream())) + } + } + + // Try upload and interrupt the call after 1 sec + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + upload, err := grpcInst.Upload(ctx, "arduino:avr:uno", sk.String(), "/dev/ttyACM0", "serial") + require.NoError(t, err) + checkFile := "" + for { + msg, err := upload.Recv() + if errors.Is(err, io.EOF) { + require.FailNow(t, "Expected interrupted upload", "upload succeded") + break + } + if err != nil { + fmt.Println("UPLOAD ERROR>", err) + break + } + if out := string(msg.GetOutStream()); out != "" { + fmt.Printf("UPLOAD OUT> %v\n", string(out)) + if strings.HasPrefix(out, "CHECKFILE: ") { + checkFile = strings.TrimSpace(out[11:]) + } + } + if msg.GetErrStream() != nil { + fmt.Printf("UPLOAD ERR> %v\n", string(msg.GetErrStream())) + } + } + cancel() + + // Wait 5 seconds. + // If the mocked avrdude is not killed it will create a checkfile and it will remove it after 5 seconds. + time.Sleep(5 * time.Second) + + // Test if the checkfile is still there (if the file is there it means that mocked avrdude + // has been correctly killed). + require.NotEmpty(t, checkFile) + require.FileExists(t, checkFile) + require.NoError(t, os.Remove(checkFile)) +} diff --git a/internal/mock_avrdude/.gitignore b/internal/mock_avrdude/.gitignore new file mode 100644 index 00000000000..7035844ce4e --- /dev/null +++ b/internal/mock_avrdude/.gitignore @@ -0,0 +1 @@ +mock_avrdude diff --git a/internal/mock_avrdude/main.go b/internal/mock_avrdude/main.go new file mode 100644 index 00000000000..3b10a1d0207 --- /dev/null +++ b/internal/mock_avrdude/main.go @@ -0,0 +1,46 @@ +// +// This file is part 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 ( + "fmt" + "os" + "time" + + "github.com/arduino/go-paths-helper" +) + +func main() { + tmp, err := paths.MkTempFile(nil, "test") + if err != nil { + fmt.Println(err) + os.Exit(1) + } + tmp.Close() + tmpPath := paths.New(tmp.Name()) + + fmt.Println("CHECKFILE:", tmpPath) + + // Just sit here for 5 seconds + time.Sleep(5 * time.Second) + + // Remove the check file at the end + tmpPath.Remove() + + fmt.Println("COMPLETED") +}