Skip to content

Commit 3687b09

Browse files
authored
fix: manage missing sketch folder (#123)
* refactor: rename MainSketchPath to mainSketchPath and update related methods * test: add tests for sketch library operations when sketch folder is missing * fix: correct description formatting in app.yaml for MissingSketch
1 parent 86853c9 commit 3687b09

File tree

7 files changed

+148
-17
lines changed

7 files changed

+148
-17
lines changed

internal/orchestrator/app/app.go

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import (
3030
type ArduinoApp struct {
3131
Name string
3232
MainPythonFile *paths.Path
33-
MainSketchPath *paths.Path
33+
mainSketchPath *paths.Path
3434
FullPath *paths.Path // FullPath is the path to the App folder
3535
Descriptor AppDescriptor
3636
}
@@ -76,10 +76,10 @@ func Load(appPath *paths.Path) (ArduinoApp, error) {
7676

7777
if appPath.Join("sketch", "sketch.ino").Exist() {
7878
// TODO: check sketch casing?
79-
app.MainSketchPath = appPath.Join("sketch")
79+
app.mainSketchPath = appPath.Join("sketch")
8080
}
8181

82-
if app.MainPythonFile == nil && app.MainSketchPath == nil {
82+
if app.MainPythonFile == nil && app.mainSketchPath == nil {
8383
return ArduinoApp{}, errors.New("main python file and sketch file missing from app")
8484
}
8585

@@ -91,6 +91,13 @@ func Load(appPath *paths.Path) (ArduinoApp, error) {
9191
return app, nil
9292
}
9393

94+
func (a *ArduinoApp) GetSketchPath() (*paths.Path, bool) {
95+
if a == nil || a.mainSketchPath == nil {
96+
return nil, false
97+
}
98+
return a.mainSketchPath, true
99+
}
100+
94101
// GetDescriptorPath returns the path to the app descriptor file (app.yaml or app.yml)
95102
func (a *ArduinoApp) GetDescriptorPath() *paths.Path {
96103
descriptorFile := a.FullPath.Join("app.yaml")

internal/orchestrator/app/app_test.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,22 @@ func TestLoad(t *testing.T) {
5858

5959
assert.NotNil(t, app.MainPythonFile)
6060
assert.Equal(t, f.Must(filepath.Abs("testdata/AppSimple/python/main.py")), app.MainPythonFile.String())
61+
sketchPath, ok := app.GetSketchPath()
62+
assert.True(t, ok)
63+
assert.NotNil(t, sketchPath)
64+
assert.Equal(t, f.Must(filepath.Abs("testdata/AppSimple/sketch")), sketchPath.String())
65+
})
66+
67+
t.Run("it loads an app with misssing sketch folder", func(t *testing.T) {
68+
app, err := Load(paths.New("testdata/MissingSketch"))
69+
assert.NoError(t, err)
70+
assert.NotEmpty(t, app)
71+
72+
assert.NotNil(t, app.MainPythonFile)
6173

62-
assert.NotNil(t, app.MainSketchPath)
63-
assert.Equal(t, f.Must(filepath.Abs("testdata/AppSimple/sketch")), app.MainSketchPath.String())
74+
sketchPath, ok := app.GetSketchPath()
75+
assert.False(t, ok)
76+
assert.Nil(t, sketchPath)
6477
})
6578
}
6679

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
name: "An app with only python"
2+
description: "An app with only python"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
2+
print("Hello world!")

internal/orchestrator/orchestrator.go

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -154,7 +154,8 @@ func StartApp(
154154
if !yield(StreamMessage{progress: &Progress{Name: "preparing", Progress: 0.0}}) {
155155
return
156156
}
157-
if appToStart.MainSketchPath != nil {
157+
158+
if _, ok := appToStart.GetSketchPath(); ok {
158159
if !yield(StreamMessage{progress: &Progress{Name: "sketch compiling and uploading", Progress: 0.0}}) {
159160
return
160161
}
@@ -175,7 +176,7 @@ func StartApp(
175176
return
176177
}
177178
provisionStartProgress := float32(0.0)
178-
if appToStart.MainSketchPath != nil {
179+
if _, ok := appToStart.GetSketchPath(); ok {
179180
provisionStartProgress = 10.0
180181
}
181182

@@ -402,7 +403,7 @@ func stopAppWithCmd(ctx context.Context, docker command.Cli, app app.ArduinoApp,
402403
}
403404
})
404405

405-
if app.MainSketchPath != nil {
406+
if _, ok := app.GetSketchPath(); ok {
406407
// Before stopping the microcontroller we want to make sure that the app was running.
407408
appStatus, err := getAppStatus(ctx, docker, app)
408409
if err != nil {
@@ -1150,9 +1151,12 @@ func compileUploadSketch(
11501151
defer func() {
11511152
_, _ = srv.Destroy(ctx, &rpc.DestroyRequest{Instance: inst})
11521153
}()
1153-
sketchPath := arduinoApp.MainSketchPath.String()
1154+
sketchPath, ok := arduinoApp.GetSketchPath()
1155+
if !ok {
1156+
return fmt.Errorf("no sketch path found in the Arduino app")
1157+
}
11541158
buildPath := arduinoApp.SketchBuildPath().String()
1155-
sketchResp, err := srv.LoadSketch(ctx, &rpc.LoadSketchRequest{SketchPath: sketchPath})
1159+
sketchResp, err := srv.LoadSketch(ctx, &rpc.LoadSketchRequest{SketchPath: sketchPath.String()})
11561160
if err != nil {
11571161
return err
11581162
}
@@ -1163,7 +1167,7 @@ func compileUploadSketch(
11631167
}
11641168
initReq := &rpc.InitRequest{
11651169
Instance: inst,
1166-
SketchPath: sketchPath,
1170+
SketchPath: sketchPath.String(),
11671171
Profile: profile,
11681172
}
11691173

@@ -1203,7 +1207,7 @@ func compileUploadSketch(
12031207
compileReq := rpc.CompileRequest{
12041208
Instance: inst,
12051209
Fqbn: "arduino:zephyr:unoq",
1206-
SketchPath: sketchPath,
1210+
SketchPath: sketchPath.String(),
12071211
BuildPath: buildPath,
12081212
Jobs: 2,
12091213
}
@@ -1229,12 +1233,12 @@ func compileUploadSketch(
12291233
slog.Info("Used library " + lib.GetName() + " (" + lib.GetVersion() + ") in " + lib.GetInstallDir())
12301234
}
12311235

1232-
if err := uploadSketchInRam(ctx, w, srv, inst, sketchPath, buildPath); err != nil {
1236+
if err := uploadSketchInRam(ctx, w, srv, inst, sketchPath.String(), buildPath); err != nil {
12331237
slog.Warn("failed to upload in ram mode, trying to configure the board in ram mode, and retry", slog.String("error", err.Error()))
12341238
if err := configureMicroInRamMode(ctx, w, srv, inst); err != nil {
12351239
return err
12361240
}
1237-
return uploadSketchInRam(ctx, w, srv, inst, sketchPath, buildPath)
1241+
return uploadSketchInRam(ctx, w, srv, inst, sketchPath.String(), buildPath)
12381242
}
12391243
return nil
12401244
}

internal/orchestrator/sketch_libs.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package orchestrator
1717

1818
import (
1919
"context"
20+
"errors"
2021
"log/slog"
2122
"time"
2223

@@ -30,6 +31,11 @@ import (
3031
const indexUpdateInterval = 10 * time.Minute
3132

3233
func AddSketchLibrary(ctx context.Context, app app.ArduinoApp, libRef LibraryReleaseID, addDeps bool) ([]LibraryReleaseID, error) {
34+
sketchPath, ok := app.GetSketchPath()
35+
if !ok {
36+
return []LibraryReleaseID{}, errors.New("cannot add a library. Missing sketch folder")
37+
}
38+
3339
srv := commands.NewArduinoCoreServer()
3440
var inst *rpc.Instance
3541
if res, err := srv.Create(ctx, &rpc.CreateRequest{}); err != nil {
@@ -58,7 +64,7 @@ func AddSketchLibrary(ctx context.Context, app app.ArduinoApp, libRef LibraryRel
5864

5965
resp, err := srv.ProfileLibAdd(ctx, &rpc.ProfileLibAddRequest{
6066
Instance: inst,
61-
SketchPath: app.MainSketchPath.String(),
67+
SketchPath: sketchPath.String(),
6268
Library: &rpc.SketchProfileLibraryReference{
6369
Library: &rpc.SketchProfileLibraryReference_IndexLibrary_{
6470
IndexLibrary: &rpc.SketchProfileLibraryReference_IndexLibrary{
@@ -77,6 +83,10 @@ func AddSketchLibrary(ctx context.Context, app app.ArduinoApp, libRef LibraryRel
7783
}
7884

7985
func RemoveSketchLibrary(ctx context.Context, app app.ArduinoApp, libRef LibraryReleaseID) (LibraryReleaseID, error) {
86+
sketchPath, ok := app.GetSketchPath()
87+
if !ok {
88+
return LibraryReleaseID{}, errors.New("cannot remove a library. Missing sketch folder")
89+
}
8090
srv := commands.NewArduinoCoreServer()
8191
var inst *rpc.Instance
8292
if res, err := srv.Create(ctx, &rpc.CreateRequest{}); err != nil {
@@ -102,7 +112,7 @@ func RemoveSketchLibrary(ctx context.Context, app app.ArduinoApp, libRef Library
102112
},
103113
},
104114
},
105-
SketchPath: app.MainSketchPath.String(),
115+
SketchPath: sketchPath.String(),
106116
})
107117
if err != nil {
108118
return LibraryReleaseID{}, err
@@ -111,10 +121,15 @@ func RemoveSketchLibrary(ctx context.Context, app app.ArduinoApp, libRef Library
111121
}
112122

113123
func ListSketchLibraries(ctx context.Context, app app.ArduinoApp) ([]LibraryReleaseID, error) {
124+
sketchPath, ok := app.GetSketchPath()
125+
if !ok {
126+
return []LibraryReleaseID{}, errors.New("cannot list libraries. Missing sketch folder")
127+
}
128+
114129
srv := commands.NewArduinoCoreServer()
115130

116131
resp, err := srv.ProfileLibList(ctx, &rpc.ProfileLibListRequest{
117-
SketchPath: app.MainSketchPath.String(),
132+
SketchPath: sketchPath.String(),
118133
})
119134
if err != nil {
120135
return nil, err
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// This file is part of arduino-app-cli.
2+
//
3+
// Copyright 2025 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-app-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 license@arduino.cc.
15+
16+
package orchestrator
17+
18+
import (
19+
"context"
20+
"testing"
21+
22+
"github.com/arduino/go-paths-helper"
23+
"github.com/stretchr/testify/assert"
24+
"github.com/stretchr/testify/require"
25+
26+
"github.com/arduino/arduino-app-cli/internal/orchestrator/app"
27+
)
28+
29+
func TestListSketchLibraries(t *testing.T) {
30+
t.Run("fail to list libraries if the sketch folder is missing", func(t *testing.T) {
31+
pythonApp, err := app.Load(createTestAppPythonOnly(t))
32+
require.NoError(t, err)
33+
34+
libs, err := ListSketchLibraries(context.Background(), pythonApp)
35+
require.Error(t, err)
36+
assert.Contains(t, err.Error(), "cannot list libraries. Missing sketch folder")
37+
assert.Empty(t, libs)
38+
})
39+
40+
t.Run("fail to add library if the sketch folder is missing", func(t *testing.T) {
41+
pythonApp, err := app.Load(createTestAppPythonOnly(t))
42+
require.NoError(t, err)
43+
44+
libs, err := AddSketchLibrary(context.Background(), pythonApp, LibraryReleaseID{}, false)
45+
require.Error(t, err)
46+
assert.Contains(t, err.Error(), "cannot add a library. Missing sketch folder")
47+
assert.Empty(t, libs)
48+
})
49+
50+
t.Run("fail to remove library if the sketch folder is missing", func(t *testing.T) {
51+
pythonApp, err := app.Load(createTestAppPythonOnly(t))
52+
require.NoError(t, err)
53+
54+
id, err := RemoveSketchLibrary(context.Background(), pythonApp, LibraryReleaseID{})
55+
require.Error(t, err)
56+
assert.Contains(t, err.Error(), "cannot remove a library. Missing sketch folder")
57+
assert.Empty(t, id)
58+
})
59+
}
60+
61+
// Helper function to create a test app without sketch path (Python-only)
62+
func createTestAppPythonOnly(t *testing.T) *paths.Path {
63+
tempDir := t.TempDir()
64+
65+
appYaml := paths.New(tempDir, "app.yaml")
66+
require.NoError(t, appYaml.WriteFile([]byte(`
67+
name: test-python-app
68+
version: 1.0.0
69+
description: Test Python-only app
70+
`)))
71+
72+
// Create python directory and file
73+
pythonDir := paths.New(tempDir, "python")
74+
require.NoError(t, pythonDir.MkdirAll())
75+
76+
pythonFile := pythonDir.Join("main.py")
77+
require.NoError(t, pythonFile.WriteFile([]byte(`
78+
import time
79+
80+
def main():
81+
print("Hello from Python!")
82+
time.sleep(1)
83+
84+
if __name__ == "__main__":
85+
main()
86+
`)))
87+
return paths.New(tempDir)
88+
}

0 commit comments

Comments
 (0)