feat(doctor): add archduino doctor subcommand

This commit is contained in:
MDietermann
2026-04-14 14:19:12 +02:00
parent 2464e70cd2
commit c710ed81df
2 changed files with 239 additions and 0 deletions

View File

@@ -0,0 +1,135 @@
package doctor
import (
"bytes"
"context"
"errors"
"strings"
"testing"
"github.com/MDietermann/ArduinoArchSetup/internal/config"
"github.com/MDietermann/ArduinoArchSetup/internal/module"
)
// convergedModule returns an empty Plan — always converged.
type convergedModule struct{ id string }
func (c convergedModule) Meta() module.Meta { return module.Meta{ID: c.id, Name: c.id} }
func (c convergedModule) Detect(context.Context) (module.State, error) { return nil, nil }
func (c convergedModule) Plan(context.Context, module.State) (module.Plan, error) {
return module.Plan{}, nil
}
func (c convergedModule) Apply(context.Context, module.Plan, module.Reporter) error { return nil }
func (c convergedModule) Verify(context.Context) error { return nil }
type pendingModule struct{ id string }
func (p pendingModule) Meta() module.Meta { return module.Meta{ID: p.id} }
func (p pendingModule) Detect(context.Context) (module.State, error) { return nil, nil }
func (p pendingModule) Plan(context.Context, module.State) (module.Plan, error) {
return module.Plan{Actions: []module.Action{{Description: "do a thing"}}}, nil
}
func (p pendingModule) Apply(context.Context, module.Plan, module.Reporter) error { return nil }
func (p pendingModule) Verify(context.Context) error { return nil }
type detectFailModule struct{ id string }
func (d detectFailModule) Meta() module.Meta { return module.Meta{ID: d.id} }
func (d detectFailModule) Detect(context.Context) (module.State, error) { return nil, errors.New("boom") }
func (d detectFailModule) Plan(context.Context, module.State) (module.Plan, error) {
return module.Plan{}, nil
}
func (d detectFailModule) Apply(context.Context, module.Plan, module.Reporter) error { return nil }
func (d detectFailModule) Verify(context.Context) error { return nil }
// notFoundRunner simulates "command not found" for version checks.
type notFoundRunner struct{}
func (notFoundRunner) Run(context.Context, string, ...string) error { return errors.New("not found") }
func (notFoundRunner) Output(context.Context, string, ...string) ([]byte, error) {
return nil, errors.New("not found")
}
// fixedRunner returns a preset version string for a matching command.
type fixedRunner struct {
arduinoOut []byte
arduinoErr error
wokwiOut []byte
wokwiErr error
}
func (f fixedRunner) Run(context.Context, string, ...string) error { return nil }
func (f fixedRunner) Output(_ context.Context, name string, args ...string) ([]byte, error) {
switch name {
case "arduino-cli":
return f.arduinoOut, f.arduinoErr
case "wokwi-cli":
return f.wokwiOut, f.wokwiErr
}
return nil, errors.New("no match")
}
func TestDoctorAllConvergedExitsZero(t *testing.T) {
reg := module.NewRegistry()
_ = reg.Register(convergedModule{id: "foo"})
var buf bytes.Buffer
code := Run(context.Background(), config.Config{}, reg, notFoundRunner{}, &buf)
if code != 0 {
t.Fatalf("exit %d, want 0; output:\n%s", code, buf.String())
}
if !strings.Contains(buf.String(), "[✓] foo") {
t.Errorf("missing converged mark: %s", buf.String())
}
}
func TestDoctorDivergedExitsOne(t *testing.T) {
reg := module.NewRegistry()
_ = reg.Register(pendingModule{id: "bar"})
var buf bytes.Buffer
code := Run(context.Background(), config.Config{}, reg, notFoundRunner{}, &buf)
if code != 1 {
t.Fatalf("exit %d, want 1", code)
}
if !strings.Contains(buf.String(), "[✗] bar") {
t.Errorf("missing diverged mark: %s", buf.String())
}
}
func TestDoctorDetectErrorIsReported(t *testing.T) {
reg := module.NewRegistry()
_ = reg.Register(detectFailModule{id: "baz"})
var buf bytes.Buffer
code := Run(context.Background(), config.Config{}, reg, notFoundRunner{}, &buf)
if code != 1 {
t.Fatalf("exit %d, want 1", code)
}
if !strings.Contains(buf.String(), "[!] baz") {
t.Errorf("missing error mark: %s", buf.String())
}
}
func TestDoctorArduinoCLIDriftIsReported(t *testing.T) {
reg := module.NewRegistry()
_ = reg.Register(convergedModule{id: "foo"})
cfg := config.Config{ArduinoCLI: config.ArduinoCLIConfig{Version: "0.35.3"}}
runner := fixedRunner{arduinoOut: []byte(`{"VersionString":"0.36.0"}`)}
var buf bytes.Buffer
code := Run(context.Background(), cfg, reg, runner, &buf)
if code != 1 {
t.Fatalf("exit %d, want 1 (drift)", code)
}
if !strings.Contains(buf.String(), "drift") {
t.Errorf("missing drift report: %s", buf.String())
}
}
func TestDoctorMissingBinariesSkipVersionCheck(t *testing.T) {
reg := module.NewRegistry()
_ = reg.Register(convergedModule{id: "foo"})
cfg := config.Config{ArduinoCLI: config.ArduinoCLIConfig{Version: "0.35.3"}}
var buf bytes.Buffer
code := Run(context.Background(), cfg, reg, notFoundRunner{}, &buf)
if code != 0 {
t.Fatalf("exit %d, want 0; output:\n%s", code, buf.String())
}
}