diff --git a/runner.go b/runner.go index 3687369..53da9b2 100644 --- a/runner.go +++ b/runner.go @@ -16,6 +16,9 @@ type Runner struct { // ErrRunnerNoService is returned when a runner was created without a service. var ErrRunnerNoService = coreerr.E("", "runner service is nil", nil) +// ErrRunnerInvalidSpecName is returned when a RunSpec name is empty or duplicated. +var ErrRunnerInvalidSpecName = coreerr.E("", "runner spec names must be non-empty and unique", nil) + // NewRunner creates a runner for the given service. func NewRunner(svc *Service) *Runner { return &Runner{service: svc} @@ -74,6 +77,9 @@ func (r *Runner) RunAll(ctx context.Context, specs []RunSpec) (*RunAllResult, er if err := r.ensureService(); err != nil { return nil, err } + if err := validateSpecs(specs); err != nil { + return nil, err + } start := time.Now() // Build dependency graph @@ -239,6 +245,9 @@ func (r *Runner) RunSequential(ctx context.Context, specs []RunSpec) (*RunAllRes if err := r.ensureService(); err != nil { return nil, err } + if err := validateSpecs(specs); err != nil { + return nil, err + } start := time.Now() results := make([]RunResult, 0, len(specs)) @@ -282,6 +291,9 @@ func (r *Runner) RunParallel(ctx context.Context, specs []RunSpec) (*RunAllResul if err := r.ensureService(); err != nil { return nil, err } + if err := validateSpecs(specs); err != nil { + return nil, err + } start := time.Now() results := make([]RunResult, len(specs)) @@ -312,3 +324,17 @@ func (r *Runner) RunParallel(ctx context.Context, specs []RunSpec) (*RunAllResul return aggResult, nil } + +func validateSpecs(specs []RunSpec) error { + seen := make(map[string]struct{}, len(specs)) + for _, spec := range specs { + if spec.Name == "" { + return coreerr.E("Runner.validateSpecs", "runner spec name is required", ErrRunnerInvalidSpecName) + } + if _, ok := seen[spec.Name]; ok { + return coreerr.E("Runner.validateSpecs", "runner spec name is duplicated", ErrRunnerInvalidSpecName) + } + seen[spec.Name] = struct{}{} + } + return nil +} diff --git a/runner_test.go b/runner_test.go index 1f160db..a6081ca 100644 --- a/runner_test.go +++ b/runner_test.go @@ -243,3 +243,33 @@ func TestRunner_NilService(t *testing.T) { require.Error(t, err) assert.ErrorIs(t, err, ErrRunnerNoService) } + +func TestRunner_InvalidSpecNames(t *testing.T) { + runner := newTestRunner(t) + + t.Run("rejects empty names", func(t *testing.T) { + _, err := runner.RunSequential(context.Background(), []RunSpec{ + {Name: "", Command: "echo", Args: []string{"a"}}, + }) + require.Error(t, err) + assert.ErrorIs(t, err, ErrRunnerInvalidSpecName) + }) + + t.Run("rejects duplicate names", func(t *testing.T) { + _, err := runner.RunAll(context.Background(), []RunSpec{ + {Name: "same", Command: "echo", Args: []string{"a"}}, + {Name: "same", Command: "echo", Args: []string{"b"}}, + }) + require.Error(t, err) + assert.ErrorIs(t, err, ErrRunnerInvalidSpecName) + }) + + t.Run("rejects duplicate names in parallel mode", func(t *testing.T) { + _, err := runner.RunParallel(context.Background(), []RunSpec{ + {Name: "one", Command: "echo", Args: []string{"a"}}, + {Name: "one", Command: "echo", Args: []string{"b"}}, + }) + require.Error(t, err) + assert.ErrorIs(t, err, ErrRunnerInvalidSpecName) + }) +}