package lifecycle import ( "context" "errors" "sync/atomic" "testing" "time" ) func TestGroupStartsAllRunnersAndCancelsSiblingsOnFirstError(t *testing.T) { started := make(chan struct{}, 2) cancelled := make(chan struct{}, 1) wantErr := errors.New("runner failed") group, err := NewGroup( runnerFunc(func(context.Context) error { started <- struct{}{} return wantErr }), runnerFunc(func(ctx context.Context) error { started <- struct{}{} <-ctx.Done() cancelled <- struct{}{} return ctx.Err() }), ) if err != nil { t.Fatalf("NewGroup(): %v", err) } if err := group.Run(context.Background()); !errors.Is(err, wantErr) { t.Fatalf("Run() error = %v, want %v", err, wantErr) } <-started <-started select { case <-cancelled: case <-time.After(time.Second): t.Fatal("sibling runner was not cancelled") } } func TestGroupWaitsForEveryRunnerBeforeReturning(t *testing.T) { release := make(chan struct{}) var exited atomic.Bool group, err := NewGroup( runnerFunc(func(context.Context) error { return errors.New("failed") }), runnerFunc(func(ctx context.Context) error { <-ctx.Done() <-release exited.Store(true) return nil }), ) if err != nil { t.Fatalf("NewGroup(): %v", err) } done := make(chan error, 1) go func() { done <- group.Run(context.Background()) }() select { case <-done: t.Fatal("Run() returned before sibling exited") case <-time.After(20 * time.Millisecond): } close(release) <-done if !exited.Load() { t.Fatal("sibling exit was not observed") } } func TestNewGroupRejectsEmptyAndTypedNilRunners(t *testing.T) { var typedNil *nilRunner for _, runners := range [][]Runner{nil, {typedNil}} { group, err := NewGroup(runners...) if err == nil || group != nil { t.Fatalf("NewGroup() = (%v, %v), want invalid group", group, err) } } } type runnerFunc func(context.Context) error func (f runnerFunc) Run(ctx context.Context) error { return f(ctx) } type nilRunner struct{} func (*nilRunner) Run(context.Context) error { return nil }