From 3a4855b92a210c451cc93e054c0d7607dc13f1ca Mon Sep 17 00:00:00 2001 From: MDietermann Date: Mon, 13 Apr 2026 20:31:43 +0200 Subject: [PATCH] feat(modules): add hello toy module as engine smoke test --- cmd/archduino/main.go | 7 ++ internal/modules/hello/hello.go | 98 ++++++++++++++++++++++++++++ internal/modules/hello/hello_test.go | 57 ++++++++++++++++ 3 files changed, 162 insertions(+) create mode 100644 internal/modules/hello/hello.go create mode 100644 internal/modules/hello/hello_test.go diff --git a/cmd/archduino/main.go b/cmd/archduino/main.go index 0fb7aaa..59744b3 100644 --- a/cmd/archduino/main.go +++ b/cmd/archduino/main.go @@ -11,6 +11,7 @@ import ( "github.com/MDietermann/ArduinoArchSetup/internal/config" "github.com/MDietermann/ArduinoArchSetup/internal/engine" "github.com/MDietermann/ArduinoArchSetup/internal/module" + "github.com/MDietermann/ArduinoArchSetup/internal/modules/hello" "github.com/MDietermann/ArduinoArchSetup/internal/privilege" "github.com/MDietermann/ArduinoArchSetup/internal/state" ) @@ -21,6 +22,12 @@ var version = "dev" // real modules into it via init(). Task 12 registers the hello toy module. var Registry = module.NewRegistry() +func init() { + if err := Registry.Register(hello.Module{}); err != nil { + panic(err) + } +} + func main() { root := &cobra.Command{ Use: "archduino", diff --git a/internal/modules/hello/hello.go b/internal/modules/hello/hello.go new file mode 100644 index 0000000..94511fe --- /dev/null +++ b/internal/modules/hello/hello.go @@ -0,0 +1,98 @@ +// Package hello is a toy module that writes a marker file. It exists to +// validate the engine end-to-end before real modules arrive in Plan 3. +package hello + +import ( + "context" + "fmt" + "os" + "path/filepath" + + "github.com/MDietermann/ArduinoArchSetup/internal/module" +) + +const defaultRelPath = ".cache/archduino/hello.ok" +const markerContent = "archduino-hello\n" + +// Module is the exported module value. Path is overridable for tests. +type Module struct { + Path string // absolute path; if empty, ~/.cache/archduino/hello.ok is used +} + +func (m Module) resolve() (string, error) { + if m.Path != "" { + return m.Path, nil + } + home, err := os.UserHomeDir() + if err != nil { + return "", err + } + return filepath.Join(home, defaultRelPath), nil +} + +func (Module) Meta() module.Meta { + return module.Meta{ + ID: "hello", + Name: "Hello", + Description: "Toy module that writes a marker file (engine smoke test).", + } +} + +type state struct{ Present bool } + +func (m Module) Detect(ctx context.Context) (module.State, error) { + path, err := m.resolve() + if err != nil { + return nil, err + } + data, err := os.ReadFile(path) + if err != nil { + if os.IsNotExist(err) { + return state{Present: false}, nil + } + return nil, err + } + return state{Present: string(data) == markerContent}, nil +} + +func (m Module) Plan(ctx context.Context, cur module.State) (module.Plan, error) { + s, _ := cur.(state) + if s.Present { + return module.Plan{}, nil + } + path, err := m.resolve() + if err != nil { + return module.Plan{}, err + } + return module.Plan{Actions: []module.Action{{ + Description: "write marker " + path, + Kind: module.ActionFileWrite, + Exec: func(ctx context.Context) error { + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return err + } + return os.WriteFile(path, []byte(markerContent), 0o644) + }, + }}}, nil +} + +func (m Module) Apply(ctx context.Context, p module.Plan, r module.Reporter) error { + for i, a := range p.Actions { + r.Progress(m.Meta().ID, i+1, len(p.Actions), a.Description) + if err := a.Exec(ctx); err != nil { + return err + } + } + return nil +} + +func (m Module) Verify(ctx context.Context) error { + st, err := m.Detect(ctx) + if err != nil { + return err + } + if !st.(state).Present { + return fmt.Errorf("hello: marker not present after apply") + } + return nil +} diff --git a/internal/modules/hello/hello_test.go b/internal/modules/hello/hello_test.go new file mode 100644 index 0000000..af93db6 --- /dev/null +++ b/internal/modules/hello/hello_test.go @@ -0,0 +1,57 @@ +package hello + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/MDietermann/ArduinoArchSetup/internal/engine" +) + +func TestHelloFullLifecycle(t *testing.T) { + path := filepath.Join(t.TempDir(), "hello.ok") + m := Module{Path: path} + ctx := context.Background() + + st, err := m.Detect(ctx) + if err != nil { + t.Fatal(err) + } + if st.(state).Present { + t.Fatal("marker should be absent on fresh tmpdir") + } + + plan, err := m.Plan(ctx, st) + if err != nil { + t.Fatal(err) + } + if plan.IsEmpty() { + t.Fatal("plan should have one action") + } + + if err := m.Apply(ctx, plan, engine.NewBufferedReporter()); err != nil { + t.Fatal(err) + } + if err := m.Verify(ctx); err != nil { + t.Fatal(err) + } + + st2, _ := m.Detect(ctx) + if !st2.(state).Present { + t.Fatal("marker should be present after apply") + } + + plan2, _ := m.Plan(ctx, st2) + if !plan2.IsEmpty() { + t.Fatal("second plan should be empty (idempotent)") + } + + data, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if string(data) != markerContent { + t.Fatalf("content = %q", data) + } +}