From 0c876a03a1a88c1759583521ed36eb8d2440ab8f Mon Sep 17 00:00:00 2001 From: mirkoCrobu Date: Mon, 15 Dec 2025 12:10:18 +0100 Subject: [PATCH 01/13] initial flasher import --- pkg/flasherapi/flasherapi.go | 88 ++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 pkg/flasherapi/flasherapi.go diff --git a/pkg/flasherapi/flasherapi.go b/pkg/flasherapi/flasherapi.go new file mode 100644 index 00000000..e73aeebd --- /dev/null +++ b/pkg/flasherapi/flasherapi.go @@ -0,0 +1,88 @@ +package flasherapi + +import ( + "context" + + "github.com/arduino/arduino-app-cli/pkg/board/remote" +) + +// GetOSImageVersion returns the version of the OS image used in the board. +// It is used by the AppLab to enforce image version compatibility. +func GetOSImageVersion(conn remote.RemoteConn) string { + // if no version is set, return a default value + return "20251123-159" +} + +type OSImageRelease struct { + VersionLabel string + ID string + Latest bool +} + +func ListAvailableOSImages() []OSImageRelease { + return []OSImageRelease{ + { + ID: "20251123-159", + VersionLabel: "r159 (2025-11-23)", + Latest: true, + }, + } +} + +func IsUserPartitionPreservationSupported(conn remote.RemoteConn, targetImageVersion OSImageRelease) bool { + // targetImageVersion is the version of the image to be flashed + // some older versions do not support user partition preservation + // so this has to be considered here + return true +} + +type FlashStep string + +const ( + FlashStepPrecheck FlashStep = "precheck" + FlashStepDetection FlashStep = "detection" + FlashStepDownloading FlashStep = "downloading" + FlashStepExtracting FlashStep = "extracting" + FlashStepFlashing FlashStep = "flashing" +) + +type FlashEvent struct { + Step FlashStep + OverallProgress int // percentage 0-100 + Message string // log message to show on the UI +} + +func Flash( + ctx context.Context, // context to cancel to interrupt the flashing process + // conn remote.RemoteConn, // is this required for the flash process? + imageVersion OSImageRelease, // OS image version to flash + preserveUserPartition bool, // whether to preserve the user partition or not + eventCB func(event FlashEvent), // Callback, sends progress events to the caller +) error { + // The disk space check is done before starting the flashing process. + return InsufficientDiskSpaceError{ + RequiredSpaceMB: 2048, + AvailableSpaceMB: 1024, + } +} + +// Errors returned by the Flash function above. +// Assertions can be done on the caller side to show better error messages. + +type QDLFlashError struct { + Details string + Cause error +} + +type DownloadError struct { + Details string +} + +type InsufficientDiskSpaceError struct { + RequiredSpaceMB int64 + AvailableSpaceMB int64 +} + +func (e InsufficientDiskSpaceError) Error() string { + return "insufficient disk space" +} From 8e59c43c18766f0fbf3d9efc08930fae3b44e136 Mon Sep 17 00:00:00 2001 From: mirkoCrobu Date: Mon, 15 Dec 2025 14:40:09 +0100 Subject: [PATCH 02/13] implement getOSImageVersion --- pkg/flasherapi/flasherapi.go | 36 ++++++++++++++++++++++++++--- pkg/flasherapi/flasherapi_test.go | 38 +++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 pkg/flasherapi/flasherapi_test.go diff --git a/pkg/flasherapi/flasherapi.go b/pkg/flasherapi/flasherapi.go index e73aeebd..62ac5b4b 100644 --- a/pkg/flasherapi/flasherapi.go +++ b/pkg/flasherapi/flasherapi.go @@ -2,15 +2,45 @@ package flasherapi import ( "context" + "log/slog" + "strings" "github.com/arduino/arduino-app-cli/pkg/board/remote" ) // GetOSImageVersion returns the version of the OS image used in the board. // It is used by the AppLab to enforce image version compatibility. -func GetOSImageVersion(conn remote.RemoteConn) string { - // if no version is set, return a default value - return "20251123-159" +func GetOSImageVersion(ctx context.Context, conn remote.RemoteConn) (string, error) { + const defaultVersion = "20250807-136" + + output, err := conn.GetCmd("cat /etc/buildinfo").Output(ctx) + if err != nil { + return defaultVersion, err + } + + if version, ok := ParseOSImageVersion(string(output)); ok { + slog.Info("find OS Image version", "version", version) + return version, nil + } + slog.Info("Unable to find OS Image version", "using default version", defaultVersion) + return defaultVersion, nil +} + +func ParseOSImageVersion(buildInfo string) (string, bool) { + for _, line := range strings.Split(buildInfo, "\n") { + line = strings.TrimSpace(line) + + key, value, ok := strings.Cut(line, "=") + if !ok || key != "BUILD_ID" { + continue + } + + version := strings.Trim(value, "\"' ") + if version != "" { + return version, true + } + } + return "", false } type OSImageRelease struct { diff --git a/pkg/flasherapi/flasherapi_test.go b/pkg/flasherapi/flasherapi_test.go new file mode 100644 index 00000000..85c44636 --- /dev/null +++ b/pkg/flasherapi/flasherapi_test.go @@ -0,0 +1,38 @@ +package flasherapi + +import "testing" + +func TestParseOSImageVersion(t *testing.T) { + tests := []struct { + name string + input string + expected string + found bool + }{ + { + name: "valid build id", + input: "BUILD_ID=\"20251006-395\"\nVARIANT_ID=xfce", + expected: "20251006-395", + found: true, + }, + { + name: "missing build id", + input: "VARIANT_ID=xfce\n", + found: false, + }, + { + name: "empty build id", + input: "BUILD_ID=\n", + found: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, ok := ParseOSImageVersion(tt.input) + if ok != tt.found || got != tt.expected { + t.Fatalf("got (%q, %v), expected (%q, %v)", got, ok, tt.expected, tt.found) + } + }) + } +} From e8c03694b0b90c6279ed10853ee5531d3c2ca82e Mon Sep 17 00:00:00 2001 From: Marta Carbone Date: Mon, 15 Dec 2025 14:49:25 +0100 Subject: [PATCH 03/13] Add a first draft of IsUserPartitionPreservationSupported --- pkg/flasherapi/flasherapi.go | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/pkg/flasherapi/flasherapi.go b/pkg/flasherapi/flasherapi.go index e73aeebd..25329051 100644 --- a/pkg/flasherapi/flasherapi.go +++ b/pkg/flasherapi/flasherapi.go @@ -1,3 +1,18 @@ +// This file is part of arduino-app-cli. +// +// Copyright 2025 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-app-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 flasherapi import ( @@ -8,7 +23,7 @@ import ( // GetOSImageVersion returns the version of the OS image used in the board. // It is used by the AppLab to enforce image version compatibility. -func GetOSImageVersion(conn remote.RemoteConn) string { +func GetOSImageVersion(ctx context.Context, conn remote.RemoteConn) string { // if no version is set, return a default value return "20251123-159" } @@ -29,11 +44,15 @@ func ListAvailableOSImages() []OSImageRelease { } } -func IsUserPartitionPreservationSupported(conn remote.RemoteConn, targetImageVersion OSImageRelease) bool { - // targetImageVersion is the version of the image to be flashed - // some older versions do not support user partition preservation - // so this has to be considered here - return true +const R0_IMAGE_VERSION_ID = "20250807-136" + +// Calculates whether user partition preservation is supported, +// according to the current and target OS image versions. +// +// Preservation is supported if both versions are not the R0 image. +func IsUserPartitionPreservationSupported(ctx context.Context, conn remote.RemoteConn, targetImageVersion OSImageRelease) bool { + currentImageVersion := GetOSImageVersion(ctx, conn) + return !(targetImageVersion.ID == R0_IMAGE_VERSION_ID || currentImageVersion == R0_IMAGE_VERSION_ID) } type FlashStep string From a23f45bad42f9f70018d0c80b763ce10ce68a8c3 Mon Sep 17 00:00:00 2001 From: mirkoCrobu Date: Mon, 15 Dec 2025 15:02:24 +0100 Subject: [PATCH 04/13] refactoring --- pkg/flasherapi/flasherapi.go | 100 +++++++----------------------- pkg/flasherapi/flasherapi_test.go | 15 +++++ 2 files changed, 36 insertions(+), 79 deletions(-) diff --git a/pkg/flasherapi/flasherapi.go b/pkg/flasherapi/flasherapi.go index 62ac5b4b..f4fea083 100644 --- a/pkg/flasherapi/flasherapi.go +++ b/pkg/flasherapi/flasherapi.go @@ -1,3 +1,18 @@ +// This file is part of arduino-app-cli. +// +// Copyright 2025 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-app-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 flasherapi import ( @@ -8,22 +23,23 @@ import ( "github.com/arduino/arduino-app-cli/pkg/board/remote" ) +const R0_IMAGE_VERSION_ID = "20250807-136" + // GetOSImageVersion returns the version of the OS image used in the board. // It is used by the AppLab to enforce image version compatibility. func GetOSImageVersion(ctx context.Context, conn remote.RemoteConn) (string, error) { - const defaultVersion = "20250807-136" - output, err := conn.GetCmd("cat /etc/buildinfo").Output(ctx) + output, err := conn.GetCmd("cat", "/etc/buildinfo").Output(ctx) if err != nil { - return defaultVersion, err + return R0_IMAGE_VERSION_ID, err } if version, ok := ParseOSImageVersion(string(output)); ok { slog.Info("find OS Image version", "version", version) return version, nil } - slog.Info("Unable to find OS Image version", "using default version", defaultVersion) - return defaultVersion, nil + slog.Info("Unable to find OS Image version", "using default version", R0_IMAGE_VERSION_ID) + return R0_IMAGE_VERSION_ID, nil } func ParseOSImageVersion(buildInfo string) (string, bool) { @@ -42,77 +58,3 @@ func ParseOSImageVersion(buildInfo string) (string, bool) { } return "", false } - -type OSImageRelease struct { - VersionLabel string - ID string - Latest bool -} - -func ListAvailableOSImages() []OSImageRelease { - return []OSImageRelease{ - { - ID: "20251123-159", - VersionLabel: "r159 (2025-11-23)", - Latest: true, - }, - } -} - -func IsUserPartitionPreservationSupported(conn remote.RemoteConn, targetImageVersion OSImageRelease) bool { - // targetImageVersion is the version of the image to be flashed - // some older versions do not support user partition preservation - // so this has to be considered here - return true -} - -type FlashStep string - -const ( - FlashStepPrecheck FlashStep = "precheck" - FlashStepDetection FlashStep = "detection" - FlashStepDownloading FlashStep = "downloading" - FlashStepExtracting FlashStep = "extracting" - FlashStepFlashing FlashStep = "flashing" -) - -type FlashEvent struct { - Step FlashStep - OverallProgress int // percentage 0-100 - Message string // log message to show on the UI -} - -func Flash( - ctx context.Context, // context to cancel to interrupt the flashing process - // conn remote.RemoteConn, // is this required for the flash process? - imageVersion OSImageRelease, // OS image version to flash - preserveUserPartition bool, // whether to preserve the user partition or not - eventCB func(event FlashEvent), // Callback, sends progress events to the caller -) error { - // The disk space check is done before starting the flashing process. - return InsufficientDiskSpaceError{ - RequiredSpaceMB: 2048, - AvailableSpaceMB: 1024, - } -} - -// Errors returned by the Flash function above. -// Assertions can be done on the caller side to show better error messages. - -type QDLFlashError struct { - Details string - Cause error -} - -type DownloadError struct { - Details string -} - -type InsufficientDiskSpaceError struct { - RequiredSpaceMB int64 - AvailableSpaceMB int64 -} - -func (e InsufficientDiskSpaceError) Error() string { - return "insufficient disk space" -} diff --git a/pkg/flasherapi/flasherapi_test.go b/pkg/flasherapi/flasherapi_test.go index 85c44636..915e8bc0 100644 --- a/pkg/flasherapi/flasherapi_test.go +++ b/pkg/flasherapi/flasherapi_test.go @@ -1,3 +1,18 @@ +// This file is part of arduino-app-cli. +// +// Copyright 2025 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-app-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 flasherapi import "testing" From 0ca5cf820a99ddeff5b1028a3b716946e7a824e5 Mon Sep 17 00:00:00 2001 From: mirkoCrobu Date: Mon, 15 Dec 2025 15:24:58 +0100 Subject: [PATCH 05/13] refactoring --- pkg/flasherapi/flasherapi.go | 34 ++++++++++++++++++++----------- pkg/flasherapi/flasherapi_test.go | 7 +++++-- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/pkg/flasherapi/flasherapi.go b/pkg/flasherapi/flasherapi.go index f4fea083..8c445f51 100644 --- a/pkg/flasherapi/flasherapi.go +++ b/pkg/flasherapi/flasherapi.go @@ -16,7 +16,9 @@ package flasherapi import ( + "bufio" "context" + "io" "log/slog" "strings" @@ -27,34 +29,42 @@ const R0_IMAGE_VERSION_ID = "20250807-136" // GetOSImageVersion returns the version of the OS image used in the board. // It is used by the AppLab to enforce image version compatibility. -func GetOSImageVersion(ctx context.Context, conn remote.RemoteConn) (string, error) { +func GetOSImageVersion(ctx context.Context, conn remote.RemoteConn) string { - output, err := conn.GetCmd("cat", "/etc/buildinfo").Output(ctx) + f, err := conn.ReadFile("/etc/buildinfo") if err != nil { - return R0_IMAGE_VERSION_ID, err + slog.Warn("Unable to read buildinfo file", "err", err, "using_default", R0_IMAGE_VERSION_ID) + return R0_IMAGE_VERSION_ID } + defer f.Close() - if version, ok := ParseOSImageVersion(string(output)); ok { - slog.Info("find OS Image version", "version", version) - return version, nil + if version, ok := parseOSImageVersion(f); ok { + slog.Info("found OS Image version", "version", version) + return version } - slog.Info("Unable to find OS Image version", "using default version", R0_IMAGE_VERSION_ID) - return R0_IMAGE_VERSION_ID, nil + slog.Warn("Unable to find OS Image version", "using_default", R0_IMAGE_VERSION_ID) + return R0_IMAGE_VERSION_ID } -func ParseOSImageVersion(buildInfo string) (string, bool) { - for _, line := range strings.Split(buildInfo, "\n") { - line = strings.TrimSpace(line) +func parseOSImageVersion(r io.Reader) (string, bool) { + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) key, value, ok := strings.Cut(line, "=") if !ok || key != "BUILD_ID" { continue } - version := strings.Trim(value, "\"' ") + version := strings.Trim(value, `"' `) if version != "" { return version, true } } + + if err := scanner.Err(); err != nil { + return "", false + } + return "", false } diff --git a/pkg/flasherapi/flasherapi_test.go b/pkg/flasherapi/flasherapi_test.go index 915e8bc0..0c6be982 100644 --- a/pkg/flasherapi/flasherapi_test.go +++ b/pkg/flasherapi/flasherapi_test.go @@ -15,7 +15,10 @@ package flasherapi -import "testing" +import ( + "strings" + "testing" +) func TestParseOSImageVersion(t *testing.T) { tests := []struct { @@ -44,7 +47,7 @@ func TestParseOSImageVersion(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got, ok := ParseOSImageVersion(tt.input) + got, ok := parseOSImageVersion(strings.NewReader(tt.input)) if ok != tt.found || got != tt.expected { t.Fatalf("got (%q, %v), expected (%q, %v)", got, ok, tt.expected, tt.found) } From 661be98f17e82dd2163e45178372b647349f4666 Mon Sep 17 00:00:00 2001 From: mirkoCrobu Date: Mon, 15 Dec 2025 17:04:53 +0100 Subject: [PATCH 06/13] code review fix --- pkg/flasherapi/flasherapi.go | 1 - pkg/flasherapi/flasherapi_test.go | 6 ++++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/flasherapi/flasherapi.go b/pkg/flasherapi/flasherapi.go index 8c445f51..142aa259 100644 --- a/pkg/flasherapi/flasherapi.go +++ b/pkg/flasherapi/flasherapi.go @@ -30,7 +30,6 @@ const R0_IMAGE_VERSION_ID = "20250807-136" // GetOSImageVersion returns the version of the OS image used in the board. // It is used by the AppLab to enforce image version compatibility. func GetOSImageVersion(ctx context.Context, conn remote.RemoteConn) string { - f, err := conn.ReadFile("/etc/buildinfo") if err != nil { slog.Warn("Unable to read buildinfo file", "err", err, "using_default", R0_IMAGE_VERSION_ID) diff --git a/pkg/flasherapi/flasherapi_test.go b/pkg/flasherapi/flasherapi_test.go index 0c6be982..36459380 100644 --- a/pkg/flasherapi/flasherapi_test.go +++ b/pkg/flasherapi/flasherapi_test.go @@ -28,8 +28,10 @@ func TestParseOSImageVersion(t *testing.T) { found bool }{ { - name: "valid build id", - input: "BUILD_ID=\"20251006-395\"\nVARIANT_ID=xfce", + name: "valid build id", + input: `BUILD_ID="20251006-395" + VARIANT_ID=xfce" + `, expected: "20251006-395", found: true, }, From 248711560866a9d5979b276a3efb608bb6dc4415 Mon Sep 17 00:00:00 2001 From: Marta Carbone Date: Mon, 15 Dec 2025 17:22:40 +0100 Subject: [PATCH 07/13] Rewrite condition, De Morgan's law --- pkg/flasherapi/flasherapi.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/flasherapi/flasherapi.go b/pkg/flasherapi/flasherapi.go index 076c4634..a031a37a 100644 --- a/pkg/flasherapi/flasherapi.go +++ b/pkg/flasherapi/flasherapi.go @@ -81,5 +81,5 @@ type OSImageRelease struct { // Preservation is supported if both versions are not the R0 image. func IsUserPartitionPreservationSupported(ctx context.Context, conn remote.RemoteConn, targetImageVersion OSImageRelease) bool { currentImageVersion := GetOSImageVersion(ctx, conn) - return !(targetImageVersion.ID == R0_IMAGE_VERSION_ID || currentImageVersion == R0_IMAGE_VERSION_ID) + return targetImageVersion.ID != R0_IMAGE_VERSION_ID && currentImageVersion != R0_IMAGE_VERSION_ID } From 016a622f21de49912ef5a035be91f23f3dd7f894 Mon Sep 17 00:00:00 2001 From: mirkoCrobu Date: Mon, 15 Dec 2025 17:40:04 +0100 Subject: [PATCH 08/13] refactoring test --- pkg/flasherapi/flasherapi_test.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pkg/flasherapi/flasherapi_test.go b/pkg/flasherapi/flasherapi_test.go index 36459380..59205234 100644 --- a/pkg/flasherapi/flasherapi_test.go +++ b/pkg/flasherapi/flasherapi_test.go @@ -18,6 +18,8 @@ package flasherapi import ( "strings" "testing" + + "github.com/stretchr/testify/require" ) func TestParseOSImageVersion(t *testing.T) { @@ -50,9 +52,8 @@ func TestParseOSImageVersion(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, ok := parseOSImageVersion(strings.NewReader(tt.input)) - if ok != tt.found || got != tt.expected { - t.Fatalf("got (%q, %v), expected (%q, %v)", got, ok, tt.expected, tt.found) - } + require.Equal(t, tt.found, ok) + require.Equal(t, tt.expected, got) }) } } From b6ea56f94d7fd5b22c7a930c98d379f79fd4a26c Mon Sep 17 00:00:00 2001 From: mirkoCrobu Date: Mon, 15 Dec 2025 17:49:26 +0100 Subject: [PATCH 09/13] remove unused parameter --- pkg/flasherapi/flasherapi.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/flasherapi/flasherapi.go b/pkg/flasherapi/flasherapi.go index 142aa259..7302d5a2 100644 --- a/pkg/flasherapi/flasherapi.go +++ b/pkg/flasherapi/flasherapi.go @@ -17,7 +17,6 @@ package flasherapi import ( "bufio" - "context" "io" "log/slog" "strings" @@ -29,7 +28,7 @@ const R0_IMAGE_VERSION_ID = "20250807-136" // GetOSImageVersion returns the version of the OS image used in the board. // It is used by the AppLab to enforce image version compatibility. -func GetOSImageVersion(ctx context.Context, conn remote.RemoteConn) string { +func GetOSImageVersion(conn remote.RemoteConn) string { f, err := conn.ReadFile("/etc/buildinfo") if err != nil { slog.Warn("Unable to read buildinfo file", "err", err, "using_default", R0_IMAGE_VERSION_ID) From 0b31374780d8da38f695ced64c22ae35409c63b7 Mon Sep 17 00:00:00 2001 From: mirkoCrobu Date: Mon, 15 Dec 2025 18:22:47 +0100 Subject: [PATCH 10/13] enhance tests --- pkg/flasherapi/flasherapi_test.go | 68 +++++++++++++++++++++++++++---- 1 file changed, 60 insertions(+), 8 deletions(-) diff --git a/pkg/flasherapi/flasherapi_test.go b/pkg/flasherapi/flasherapi_test.go index 59205234..c904da34 100644 --- a/pkg/flasherapi/flasherapi_test.go +++ b/pkg/flasherapi/flasherapi_test.go @@ -12,16 +12,62 @@ // 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 flasherapi import ( + "context" + "io" "strings" "testing" "github.com/stretchr/testify/require" + + "github.com/arduino/arduino-app-cli/pkg/board/remote" ) +// implements remote.RemoteConn +type MockRemoteConn struct { + ReadFileFunc func(path string) (io.ReadCloser, error) +} + +func (m *MockRemoteConn) ReadFile(path string) (io.ReadCloser, error) { + return m.ReadFileFunc(path) +} + +// Empty definitions +func (m *MockRemoteConn) List(path string) ([]remote.FileInfo, error) { + return nil, nil +} +func (m *MockRemoteConn) MkDirAll(path string) error { + return nil +} +func (m *MockRemoteConn) Remove(path string) error { + return nil +} +func (m *MockRemoteConn) Stats(path string) (remote.FileInfo, error) { + return remote.FileInfo{}, nil +} +func (m *MockRemoteConn) WriteFile(data io.Reader, path string) error { + return nil +} +func (m *MockRemoteConn) GetCmd(cmd string, args ...string) remote.Cmder { + return nil +} +func (m *MockRemoteConn) Forward(ctx context.Context, localPort int, remotePort int) error { + return nil +} +func (m *MockRemoteConn) ForwardKillAll(ctx context.Context) error { + return nil +} +func createBuildInfoConnection(imageVersion string) remote.RemoteConn { + mockConn := MockRemoteConn{ + ReadFileFunc: func(path string) (io.ReadCloser, error) { + return io.NopCloser(strings.NewReader(imageVersion)), nil + }, + } + return &mockConn +} + func TestParseOSImageVersion(t *testing.T) { tests := []struct { name string @@ -30,10 +76,8 @@ func TestParseOSImageVersion(t *testing.T) { found bool }{ { - name: "valid build id", - input: `BUILD_ID="20251006-395" - VARIANT_ID=xfce" - `, + name: "valid build id", + input: "BUILD_ID=\"20251006-395\"\nVARIANT_ID=xfce", expected: "20251006-395", found: true, }, @@ -48,12 +92,20 @@ func TestParseOSImageVersion(t *testing.T) { found: false, }, } - for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, ok := parseOSImageVersion(strings.NewReader(tt.input)) - require.Equal(t, tt.found, ok) - require.Equal(t, tt.expected, got) + if ok != tt.found || got != tt.expected { + t.Fatalf("got (%q, %v), expected (%q, %v)", got, ok, tt.expected, tt.found) + } }) } } + +func TestGetOSImageVersion(t *testing.T) { + const R0_IMAGE_VERSION_ID = "20250807-136" + R0Version := createBuildInfoConnection(R0_IMAGE_VERSION_ID) + AnotherVersion := createBuildInfoConnection("BUILD_ID=20250101-001") + require.Equal(t, GetOSImageVersion(R0Version), R0_IMAGE_VERSION_ID) + require.Equal(t, GetOSImageVersion(AnotherVersion), "20250101-001") +} From 5f4e9e81288f22165ae183e0f7c550d4861e42ea Mon Sep 17 00:00:00 2001 From: Marta Carbone Date: Mon, 15 Dec 2025 18:25:49 +0100 Subject: [PATCH 11/13] Remove context, add tests --- pkg/flasherapi/flasherapi.go | 4 +-- pkg/flasherapi/flasherapi_test.go | 46 +++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/pkg/flasherapi/flasherapi.go b/pkg/flasherapi/flasherapi.go index 3c79faf8..501acdfa 100644 --- a/pkg/flasherapi/flasherapi.go +++ b/pkg/flasherapi/flasherapi.go @@ -77,7 +77,7 @@ type OSImageRelease struct { // according to the current and target OS image versions. // // Preservation is supported if both versions are not the R0 image. -func IsUserPartitionPreservationSupported(ctx context.Context, conn remote.RemoteConn, targetImageVersion OSImageRelease) bool { - currentImageVersion := GetOSImageVersion(ctx, conn) +func IsUserPartitionPreservationSupported(conn remote.RemoteConn, targetImageVersion OSImageRelease) bool { + currentImageVersion := GetOSImageVersion(conn) return targetImageVersion.ID != R0_IMAGE_VERSION_ID && currentImageVersion != R0_IMAGE_VERSION_ID } diff --git a/pkg/flasherapi/flasherapi_test.go b/pkg/flasherapi/flasherapi_test.go index c904da34..a8ea83cc 100644 --- a/pkg/flasherapi/flasherapi_test.go +++ b/pkg/flasherapi/flasherapi_test.go @@ -109,3 +109,49 @@ func TestGetOSImageVersion(t *testing.T) { require.Equal(t, GetOSImageVersion(R0Version), R0_IMAGE_VERSION_ID) require.Equal(t, GetOSImageVersion(AnotherVersion), "20250101-001") } + +func TestIsUserPartitionPreservationSupported(t *testing.T) { + const R0_IMAGE_VERSION_ID = "20250807-136" + + R0Version := createBuildInfoConnection(R0_IMAGE_VERSION_ID) + AnotherVersion := createBuildInfoConnection("BUILD_ID=20250101-001") + + tests := []struct { + name string + currentVersion remote.RemoteConn + targetVersion string + isPreservationSupported bool + }{ + { + name: "both versions are *not* R0", + currentVersion: AnotherVersion, + targetVersion: "20250101-001", + isPreservationSupported: true, + }, + { + name: "current version is R0", + currentVersion: R0Version, + targetVersion: "20250101-001", + isPreservationSupported: false, + }, + { + name: "target version is R0", + currentVersion: AnotherVersion, + targetVersion: R0_IMAGE_VERSION_ID, + isPreservationSupported: false, + }, + { + name: "both versions are R0", + currentVersion: R0Version, + targetVersion: R0_IMAGE_VERSION_ID, + isPreservationSupported: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + isPreservationSupported := IsUserPartitionPreservationSupported(tt.currentVersion, OSImageRelease{ID: tt.targetVersion}) + require.Equal(t, isPreservationSupported, tt.isPreservationSupported) + }) + } +} From 5454fa7fda1602261b08ef96cfc6313a7e0b2866 Mon Sep 17 00:00:00 2001 From: mirkoCrobu Date: Mon, 15 Dec 2025 18:27:48 +0100 Subject: [PATCH 12/13] move os_image funcs into new file --- pkg/{flasherapi/flasherapi.go => board/os_image.go} | 2 +- pkg/{flasherapi/flasherapi_test.go => board/os_image_test.go} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename pkg/{flasherapi/flasherapi.go => board/os_image.go} (99%) rename pkg/{flasherapi/flasherapi_test.go => board/os_image_test.go} (99%) diff --git a/pkg/flasherapi/flasherapi.go b/pkg/board/os_image.go similarity index 99% rename from pkg/flasherapi/flasherapi.go rename to pkg/board/os_image.go index 7302d5a2..c9777964 100644 --- a/pkg/flasherapi/flasherapi.go +++ b/pkg/board/os_image.go @@ -13,7 +13,7 @@ // Arduino software without disclosing the source code of your own applications. // To purchase a commercial license, send an email to license@arduino.cc. -package flasherapi +package board import ( "bufio" diff --git a/pkg/flasherapi/flasherapi_test.go b/pkg/board/os_image_test.go similarity index 99% rename from pkg/flasherapi/flasherapi_test.go rename to pkg/board/os_image_test.go index c904da34..2862e126 100644 --- a/pkg/flasherapi/flasherapi_test.go +++ b/pkg/board/os_image_test.go @@ -12,7 +12,7 @@ // 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 flasherapi +package board import ( "context" From 3d6c11a3c02e5dc38635a0ef84b8894d4bab9b0d Mon Sep 17 00:00:00 2001 From: Marta Carbone Date: Tue, 16 Dec 2025 12:01:04 +0100 Subject: [PATCH 13/13] Address review --- pkg/board/os_image.go | 11 ++--------- pkg/board/os_image_test.go | 16 +++++++--------- 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/pkg/board/os_image.go b/pkg/board/os_image.go index bd5eae9f..ee52fa08 100644 --- a/pkg/board/os_image.go +++ b/pkg/board/os_image.go @@ -67,17 +67,10 @@ func parseOSImageVersion(r io.Reader) (string, bool) { return "", false } -type OSImageRelease struct { - VersionLabel string - ID string - Latest bool -} - // Calculates whether user partition preservation is supported, // according to the current and target OS image versions. // // Preservation is supported if both versions are not the R0 image. -func IsUserPartitionPreservationSupported(conn remote.RemoteConn, targetImageVersion OSImageRelease) bool { - currentImageVersion := GetOSImageVersion(conn) - return targetImageVersion.ID != R0_IMAGE_VERSION_ID && currentImageVersion != R0_IMAGE_VERSION_ID +func IsUserPartitionPreservationSupported(currentImageVersion string, targetImageVersion string) bool { + return targetImageVersion != R0_IMAGE_VERSION_ID && currentImageVersion != R0_IMAGE_VERSION_ID } diff --git a/pkg/board/os_image_test.go b/pkg/board/os_image_test.go index aa7a7626..948ccfb8 100644 --- a/pkg/board/os_image_test.go +++ b/pkg/board/os_image_test.go @@ -112,37 +112,35 @@ func TestGetOSImageVersion(t *testing.T) { func TestIsUserPartitionPreservationSupported(t *testing.T) { const R0_IMAGE_VERSION_ID = "20250807-136" - - R0Version := createBuildInfoConnection(R0_IMAGE_VERSION_ID) - AnotherVersion := createBuildInfoConnection("BUILD_ID=20250101-001") + anotherVersionId := "20250101-001" tests := []struct { name string - currentVersion remote.RemoteConn + currentVersion string targetVersion string isPreservationSupported bool }{ { name: "both versions are *not* R0", - currentVersion: AnotherVersion, + currentVersion: anotherVersionId, targetVersion: "20250101-001", isPreservationSupported: true, }, { name: "current version is R0", - currentVersion: R0Version, + currentVersion: R0_IMAGE_VERSION_ID, targetVersion: "20250101-001", isPreservationSupported: false, }, { name: "target version is R0", - currentVersion: AnotherVersion, + currentVersion: anotherVersionId, targetVersion: R0_IMAGE_VERSION_ID, isPreservationSupported: false, }, { name: "both versions are R0", - currentVersion: R0Version, + currentVersion: R0_IMAGE_VERSION_ID, targetVersion: R0_IMAGE_VERSION_ID, isPreservationSupported: false, }, @@ -150,7 +148,7 @@ func TestIsUserPartitionPreservationSupported(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - isPreservationSupported := IsUserPartitionPreservationSupported(tt.currentVersion, OSImageRelease{ID: tt.targetVersion}) + isPreservationSupported := IsUserPartitionPreservationSupported(tt.currentVersion, tt.targetVersion) require.Equal(t, isPreservationSupported, tt.isPreservationSupported) }) }