Skip to content

Commit ef667b1

Browse files
committed
add unit test for brick details
1 parent ebda234 commit ef667b1

File tree

2 files changed

+180
-9
lines changed

2 files changed

+180
-9
lines changed

internal/orchestrator/bricks/bricks_test.go

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
package bricks
1717

1818
import (
19+
"os"
20+
"path/filepath"
1921
"testing"
2022

2123
"github.com/arduino/go-paths-helper"
@@ -24,6 +26,9 @@ import (
2426

2527
"github.com/arduino/arduino-app-cli/internal/orchestrator/app"
2628
"github.com/arduino/arduino-app-cli/internal/orchestrator/bricksindex"
29+
"github.com/arduino/arduino-app-cli/internal/orchestrator/config"
30+
"github.com/arduino/arduino-app-cli/internal/orchestrator/modelsindex"
31+
"github.com/arduino/arduino-app-cli/internal/store"
2732
)
2833

2934
func TestBrickCreate(t *testing.T) {
@@ -190,3 +195,169 @@ func TestGetBrickInstanceVariableDetails(t *testing.T) {
190195
})
191196
}
192197
}
198+
199+
func TestBricksDetails(t *testing.T) {
200+
tmpDir := t.TempDir()
201+
appsDir := filepath.Join(tmpDir, "ArduinoApps")
202+
dataDir := filepath.Join(tmpDir, "Data")
203+
assetsDir := filepath.Join(dataDir, "assets")
204+
205+
require.NoError(t, os.MkdirAll(appsDir, 0755))
206+
require.NoError(t, os.MkdirAll(assetsDir, 0755))
207+
208+
t.Setenv("ARDUINO_APP_CLI__APPS_DIR", appsDir)
209+
t.Setenv("ARDUINO_APP_CLI__DATA_DIR", dataDir)
210+
211+
cfg, err := config.NewFromEnv()
212+
require.NoError(t, err)
213+
214+
for _, brick := range []string{"object_detection", "weather_forecast", "one_model_brick"} {
215+
createFakeBrickAssets(t, assetsDir, brick)
216+
}
217+
createFakeApp(t, appsDir)
218+
219+
bIndex := &bricksindex.BricksIndex{
220+
Bricks: []bricksindex.Brick{
221+
{
222+
ID: "arduino:object_detection",
223+
Name: "Object Detection",
224+
Category: "video",
225+
ModelName: "yolox-object-detection", // Default model
226+
Variables: []bricksindex.BrickVariable{
227+
{Name: "EI_OBJ_DETECTION_MODEL", DefaultValue: "default_path", Description: "path to the model file"},
228+
{Name: "CUSTOM_MODEL_PATH", DefaultValue: "/home/arduino/.arduino-bricks/ei-models", Description: "path to the custom model directory"},
229+
},
230+
},
231+
{
232+
ID: "arduino:weather_forecast",
233+
Name: "Weather Forecast",
234+
Category: "miscellaneous",
235+
ModelName: "",
236+
},
237+
{
238+
ID: "arduino:one_model_brick",
239+
Name: "one model brick",
240+
Category: "video",
241+
ModelName: "face-detection", // Default model
242+
Variables: []bricksindex.BrickVariable{},
243+
},
244+
},
245+
}
246+
mIndex := &modelsindex.ModelsIndex{
247+
Models: []modelsindex.AIModel{
248+
249+
{
250+
ID: "yolox-object-detection",
251+
Name: "General purpose object detection - YoloX",
252+
ModuleDescription: "General purpose object detection...",
253+
Bricks: []string{"arduino:object_detection", "arduino:video_object_detection"},
254+
},
255+
{
256+
ID: "face-detection",
257+
Name: "Lightweight-Face-Detection",
258+
Bricks: []string{"arduino:object_detection", "arduino:video_object_detection", "arduino:one_model_brick"},
259+
},
260+
}}
261+
262+
svc := &Service{
263+
bricksIndex: bIndex,
264+
modelsIndex: mIndex,
265+
staticStore: store.NewStaticStore(assetsDir),
266+
}
267+
idProvider := app.NewAppIDProvider(cfg)
268+
269+
t.Run("Brick Not Found", func(t *testing.T) {
270+
res, err := svc.BricksDetails("arduino:non_existing", idProvider, cfg)
271+
require.Error(t, err)
272+
require.Equal(t, ErrBrickNotFound, err)
273+
require.Empty(t, res.ID)
274+
})
275+
276+
t.Run("Success - Full Details - multiple models", func(t *testing.T) {
277+
res, err := svc.BricksDetails("arduino:object_detection", idProvider, cfg)
278+
require.NoError(t, err)
279+
280+
require.Equal(t, "arduino:object_detection", res.ID)
281+
require.Equal(t, "Object Detection", res.Name)
282+
require.Equal(t, "Arduino", res.Author)
283+
require.Equal(t, "installed", res.Status)
284+
require.Contains(t, res.Variables, "EI_OBJ_DETECTION_MODEL")
285+
require.Equal(t, "default_path", res.Variables["EI_OBJ_DETECTION_MODEL"].DefaultValue)
286+
require.Equal(t, "# Documentation", res.Readme)
287+
require.Contains(t, res.ApiDocsPath, filepath.Join("arduino", "app_bricks", "object_detection", "API.md"))
288+
require.Len(t, res.CodeExamples, 1)
289+
require.Contains(t, res.CodeExamples[0].Path, "blink.ino")
290+
require.Len(t, res.UsedByApps, 1)
291+
require.Equal(t, "My App", res.UsedByApps[0].Name)
292+
require.NotEmpty(t, res.UsedByApps[0].ID)
293+
require.Len(t, res.Models, 2)
294+
require.Equal(t, "yolox-object-detection", res.Models[0].ID)
295+
require.Equal(t, "General purpose object detection - YoloX", res.Models[0].Name)
296+
require.Equal(t, "General purpose object detection...", res.Models[0].Description)
297+
require.Equal(t, "face-detection", res.Models[1].ID)
298+
require.Equal(t, "Lightweight-Face-Detection", res.Models[1].Name)
299+
require.Equal(t, "", res.Models[1].Description)
300+
})
301+
302+
t.Run("Success - Full Details - no models", func(t *testing.T) {
303+
res, err := svc.BricksDetails("arduino:weather_forecast", idProvider, cfg)
304+
require.NoError(t, err)
305+
306+
require.Equal(t, "arduino:weather_forecast", res.ID)
307+
require.Equal(t, "Weather Forecast", res.Name)
308+
require.Equal(t, "Arduino", res.Author)
309+
require.Equal(t, "installed", res.Status)
310+
require.Empty(t, res.Variables)
311+
require.Equal(t, "# Documentation", res.Readme)
312+
require.Contains(t, res.ApiDocsPath, filepath.Join("arduino", "app_bricks", "weather_forecast", "API.md"))
313+
require.Len(t, res.CodeExamples, 1)
314+
require.Contains(t, res.CodeExamples[0].Path, "blink.ino")
315+
require.Len(t, res.UsedByApps, 1)
316+
require.Equal(t, "My App", res.UsedByApps[0].Name)
317+
require.NotEmpty(t, res.UsedByApps[0].ID)
318+
require.Len(t, res.Models, 0)
319+
})
320+
321+
t.Run("Success - Full Details - one model", func(t *testing.T) {
322+
res, err := svc.BricksDetails("arduino:one_model_brick", idProvider, cfg)
323+
require.NoError(t, err)
324+
325+
require.Equal(t, "arduino:one_model_brick", res.ID)
326+
require.Equal(t, "one model brick", res.Name)
327+
require.Len(t, res.Models, 1)
328+
require.Equal(t, "face-detection", res.Models[0].ID)
329+
require.Equal(t, "Lightweight-Face-Detection", res.Models[0].Name)
330+
require.Equal(t, "", res.Models[0].Description)
331+
})
332+
}
333+
334+
func createFakeBrickAssets(t *testing.T, assetsDir, brick string) {
335+
t.Helper()
336+
337+
brickDocDir := filepath.Join(assetsDir, "docs", "arduino", brick)
338+
require.NoError(t, os.MkdirAll(brickDocDir, 0755))
339+
require.NoError(t, os.WriteFile(filepath.Join(brickDocDir, "README.md"),
340+
[]byte("# Documentation"), 0600))
341+
342+
brickExDir := filepath.Join(assetsDir, "examples", "arduino", brick)
343+
require.NoError(t, os.MkdirAll(brickExDir, 0755))
344+
require.NoError(t, os.WriteFile(filepath.Join(brickExDir, "blink.ino"),
345+
[]byte("void setup() {}"), 0600))
346+
}
347+
348+
func createFakeApp(t *testing.T, appsDir string) {
349+
t.Helper()
350+
myAppDir := filepath.Join(appsDir, "MyApp")
351+
require.NoError(t, os.MkdirAll(myAppDir, 0755))
352+
353+
appYamlContent := `
354+
name: My App
355+
bricks:
356+
- arduino:object_detection:
357+
- arduino:weather_forecast:
358+
`
359+
require.NoError(t, os.WriteFile(filepath.Join(myAppDir, "app.yaml"), []byte(appYamlContent), 0600))
360+
pythonDir := filepath.Join(myAppDir, "python")
361+
require.NoError(t, os.MkdirAll(pythonDir, 0755))
362+
require.NoError(t, os.WriteFile(filepath.Join(pythonDir, "main.py"), []byte("print('hello')"), 0600))
363+
}

internal/orchestrator/modelsindex/models_index.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -54,26 +54,26 @@ type AIModel struct {
5454
}
5555

5656
type ModelsIndex struct {
57-
models []AIModel
57+
Models []AIModel
5858
}
5959

6060
func (m *ModelsIndex) GetModels() []AIModel {
61-
return m.models
61+
return m.Models
6262
}
6363

6464
func (m *ModelsIndex) GetModelByID(id string) (*AIModel, bool) {
65-
idx := slices.IndexFunc(m.models, func(v AIModel) bool { return v.ID == id })
65+
idx := slices.IndexFunc(m.Models, func(v AIModel) bool { return v.ID == id })
6666
if idx == -1 {
6767
return nil, false
6868
}
69-
return &m.models[idx], true
69+
return &m.Models[idx], true
7070
}
7171

7272
func (m *ModelsIndex) GetModelsByBrick(brick string) []AIModel {
7373
var matches []AIModel
74-
for i := range m.models {
75-
if len(m.models[i].Bricks) > 0 && slices.Contains(m.models[i].Bricks, brick) {
76-
matches = append(matches, m.models[i])
74+
for i := range m.Models {
75+
if len(m.Models[i].Bricks) > 0 && slices.Contains(m.Models[i].Bricks, brick) {
76+
matches = append(matches, m.Models[i])
7777
}
7878
}
7979
if len(matches) == 0 {
@@ -84,7 +84,7 @@ func (m *ModelsIndex) GetModelsByBrick(brick string) []AIModel {
8484

8585
func (m *ModelsIndex) GetModelsByBricks(bricks []string) []AIModel {
8686
var matchingModels []AIModel
87-
for _, model := range m.models {
87+
for _, model := range m.Models {
8888
for _, modelBrick := range model.Bricks {
8989
if slices.Contains(bricks, modelBrick) {
9090
matchingModels = append(matchingModels, model)
@@ -113,5 +113,5 @@ func GenerateModelsIndexFromFile(dir *paths.Path) (*ModelsIndex, error) {
113113
models[i] = model
114114
}
115115
}
116-
return &ModelsIndex{models: models}, nil
116+
return &ModelsIndex{Models: models}, nil
117117
}

0 commit comments

Comments
 (0)