package arduinocli import ( "archive/tar" "compress/gzip" "context" "net/http" "net/http/httptest" "os" "path/filepath" "testing" "gitlab.hive-brain.de/MDietermann/Archduino/internal/config" "gitlab.hive-brain.de/MDietermann/Archduino/internal/module" ) type fakeRunner struct { runCalls [][]string outCalls [][]string coreListOut []byte } func (f *fakeRunner) Run(_ context.Context, name string, args ...string) error { f.runCalls = append(f.runCalls, append([]string{name}, args...)) return nil } func (f *fakeRunner) Output(_ context.Context, name string, args ...string) ([]byte, error) { f.outCalls = append(f.outCalls, append([]string{name}, args...)) if filepath.Base(name) == "arduino-cli" && len(args) >= 2 && args[0] == "core" && args[1] == "list" { return f.coreListOut, nil } return nil, nil } type nop struct{} func (nop) Progress(string, int, int, string) {} func (nop) Log(string, string) {} func makeTarGz(t *testing.T, entries map[string][]byte) string { t.Helper() p := filepath.Join(t.TempDir(), "a.tgz") f, _ := os.Create(p) gz := gzip.NewWriter(f) tw := tar.NewWriter(gz) for name, data := range entries { _ = tw.WriteHeader(&tar.Header{Name: name, Mode: 0o755, Size: int64(len(data))}) _, _ = tw.Write(data) } tw.Close() gz.Close() f.Close() return p } func TestArduinoCLIDownloadsAndInstallsCore(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) archive := makeTarGz(t, map[string][]byte{"arduino-cli": []byte("#!/bin/sh\n")}) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { data, _ := os.ReadFile(archive) w.Write(data) })) defer srv.Close() r := &fakeRunner{coreListOut: []byte("ID Installed\n")} m := New(config.ArduinoCLIConfig{ Version: "1.0.0", URL: srv.URL + "/arduino-cli.tgz", Cores: []string{"arduino:avr"}, }, r) st, _ := m.Detect(context.Background()) p, _ := m.Plan(context.Background(), st) if err := m.Apply(context.Background(), p, nop{}); err != nil { t.Fatal(err) } if _, err := os.Stat(filepath.Join(home, ".local", "bin", "arduino-cli")); err != nil { t.Fatalf("binary not installed: %v", err) } sawCore := false for _, c := range r.runCalls { if filepath.Base(c[0]) == "arduino-cli" && len(c) >= 4 && c[1] == "core" && c[2] == "install" && c[3] == "arduino:avr" { sawCore = true } } if !sawCore { t.Fatalf("core install not run; calls=%v", r.runCalls) } } func TestPlanForceRedownloadsAndReinstallsCores(t *testing.T) { home := t.TempDir() t.Setenv("HOME", home) // Pre-populate a stale binary to prove force overwrites. os.MkdirAll(filepath.Join(home, ".local", "bin"), 0o755) bp := filepath.Join(home, ".local", "bin", "arduino-cli") os.WriteFile(bp, []byte("stale"), 0o755) archive := makeTarGz(t, map[string][]byte{"arduino-cli": []byte("#!/bin/sh\nfresh\n")}) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { data, _ := os.ReadFile(archive) w.Write(data) })) defer srv.Close() r := &fakeRunner{coreListOut: []byte("arduino:avr\nesp32:esp32\n")} m := New(config.ArduinoCLIConfig{ Version: "1.0.0", URL: srv.URL + "/arduino-cli.tgz", Cores: []string{"arduino:avr", "esp32:esp32"}, }, r) plan, err := m.PlanForce(context.Background()) if err != nil { t.Fatalf("PlanForce: %v", err) } if plan.IsEmpty() { t.Fatal("force plan must not be empty") } for _, a := range plan.Actions { if err := a.Exec(context.Background()); err != nil { t.Fatalf("Exec %q: %v", a.Description, err) } } // Binary got overwritten with fresh payload. got, _ := os.ReadFile(bp) if string(got) == "stale" { t.Fatalf("binary not overwritten by force") } // update-index and both `core install` calls fired. sawUpdate := false installed := map[string]bool{} for _, c := range r.runCalls { if filepath.Base(c[0]) == "arduino-cli" && len(c) >= 3 && c[1] == "core" && c[2] == "update-index" { sawUpdate = true } if filepath.Base(c[0]) == "arduino-cli" && len(c) >= 4 && c[1] == "core" && c[2] == "install" { installed[c[3]] = true } } if !sawUpdate { t.Errorf("no core update-index call; calls=%v", r.runCalls) } if !installed["arduino:avr"] || !installed["esp32:esp32"] { t.Errorf("expected both cores force-installed; got=%v calls=%v", installed, r.runCalls) } } var _ module.Module = Module{}