package engine import ( "context" "errors" "testing" "gitlab.hive-brain.de/MDietermann/Archduino/internal/module" ) // scriptedModule lets tests control what each phase returns. type scriptedModule struct { id string deps []string detect func() (module.State, error) plan func() (module.Plan, error) planForce func() (module.Plan, error) apply func() error verify func() error detectCalls int planCalls int planForceCalls int } func (s *scriptedModule) Meta() module.Meta { return module.Meta{ID: s.id, Name: s.id, Deps: s.deps} } func (s *scriptedModule) Detect(ctx context.Context) (module.State, error) { s.detectCalls++ if s.detect != nil { return s.detect() } return nil, nil } func (s *scriptedModule) Plan(ctx context.Context, _ module.State) (module.Plan, error) { s.planCalls++ if s.plan != nil { return s.plan() } return module.Plan{}, nil } func (s *scriptedModule) PlanForce(ctx context.Context) (module.Plan, error) { s.planForceCalls++ if s.planForce != nil { return s.planForce() } if s.plan != nil { return s.plan() } return module.Plan{}, nil } func (s *scriptedModule) Apply(ctx context.Context, _ module.Plan, _ module.Reporter) error { if s.apply != nil { return s.apply() } return nil } func (s *scriptedModule) Verify(ctx context.Context) error { if s.verify != nil { return s.verify() } return nil } func newReg(t *testing.T, mods ...module.Module) *module.Registry { t.Helper() r := module.NewRegistry() for _, m := range mods { if err := r.Register(m); err != nil { t.Fatal(err) } } return r } func TestRunAllSucceed(t *testing.T) { reg := newReg(t, &scriptedModule{id: "a"}, &scriptedModule{id: "b", deps: []string{"a"}}, ) sum, err := Run(context.Background(), reg, RunOptions{Reporter: NewBufferedReporter()}) if err != nil { t.Fatal(err) } ok, fail, skip := sum.Counts() if ok != 2 || fail != 0 || skip != 0 { t.Fatalf("counts = (%d,%d,%d)", ok, fail, skip) } if sum.Results[0].ID != "a" || sum.Results[1].ID != "b" { t.Fatalf("order = %v", sum.Results) } } func TestRunDryRunDoesNotApply(t *testing.T) { applied := false reg := newReg(t, &scriptedModule{ id: "a", apply: func() error { applied = true; return nil }, }) sum, err := Run(context.Background(), reg, RunOptions{DryRun: true, Reporter: NewBufferedReporter()}) if err != nil { t.Fatal(err) } if applied { t.Fatal("Apply was called during dry-run") } if sum.Results[0].Status != StatusPlanned { t.Fatalf("status = %v", sum.Results[0].Status) } } func TestRunFailureSkipsDescendantsContinuesSiblings(t *testing.T) { reg := newReg(t, &scriptedModule{id: "root"}, &scriptedModule{id: "fails", deps: []string{"root"}, apply: func() error { return errors.New("boom") }}, &scriptedModule{id: "child", deps: []string{"fails"}}, &scriptedModule{id: "sibling", deps: []string{"root"}}, ) sum, err := Run(context.Background(), reg, RunOptions{Reporter: NewBufferedReporter()}) if err != nil { t.Fatal(err) } status := map[string]ModuleStatus{} for _, r := range sum.Results { status[r.ID] = r.Status } if status["root"] != StatusSucceeded { t.Errorf("root = %v", status["root"]) } if status["fails"] != StatusFailed { t.Errorf("fails = %v", status["fails"]) } if status["child"] != StatusSkipped { t.Errorf("child = %v", status["child"]) } if status["sibling"] != StatusSucceeded { t.Errorf("sibling = %v (should have run)", status["sibling"]) } } func TestRunHaltOnError(t *testing.T) { reg := newReg(t, &scriptedModule{id: "a", apply: func() error { return errors.New("nope") }}, &scriptedModule{id: "b"}, // independent ) sum, err := Run(context.Background(), reg, RunOptions{HaltOnError: true, Reporter: NewBufferedReporter()}) if err != nil { t.Fatal(err) } // "b" should not appear in results because we halted before reaching it. for _, r := range sum.Results { if r.ID == "b" { t.Fatalf("b was run despite HaltOnError") } } } func TestRunUnknownModule(t *testing.T) { reg := module.NewRegistry() _, err := Run(context.Background(), reg, RunOptions{Selected: []string{"ghost"}}) if err == nil { t.Fatal("expected error for unknown module") } } func TestRunForceSkipsDetectAndCallsPlanForce(t *testing.T) { mod := &scriptedModule{ id: "a", detect: func() (module.State, error) { panic("Detect must not be called when Force=true") }, planForce: func() (module.Plan, error) { return module.Plan{Actions: []module.Action{{Description: "forced"}}}, nil }, } reg := newReg(t, mod) sum, err := Run(context.Background(), reg, RunOptions{Force: true, Reporter: NewBufferedReporter()}) if err != nil { t.Fatal(err) } if mod.planForceCalls != 1 { t.Errorf("planForceCalls = %d, want 1", mod.planForceCalls) } if mod.detectCalls != 0 { t.Errorf("detectCalls = %d, want 0", mod.detectCalls) } if mod.planCalls != 0 { t.Errorf("planCalls = %d, want 0", mod.planCalls) } ok, fail, skip := sum.Counts() if ok != 1 || fail != 0 || skip != 0 { t.Fatalf("counts = (%d,%d,%d)", ok, fail, skip) } if len(sum.Results[0].Plan.Actions) != 1 { t.Fatalf("expected 1 action in plan, got %d", len(sum.Results[0].Plan.Actions)) } }