diff --git a/cmd/entire/cli/errors.go b/cmd/entire/cli/errors.go index 36e510d14..81764aeb5 100644 --- a/cmd/entire/cli/errors.go +++ b/cmd/entire/cli/errors.go @@ -20,3 +20,25 @@ func (e *SilentError) Unwrap() error { func NewSilentError(err error) *SilentError { return &SilentError{Err: err} } + +// ExitCodeError wraps an error with a specific process exit code. +// Use this when a subcommand needs to exit with a code other than 1. +// main.go checks for this type and uses ExitCode instead of the default 1. +type ExitCodeError struct { + Err error + ExitCode int +} + +func (e *ExitCodeError) Error() string { + return e.Err.Error() +} + +func (e *ExitCodeError) Unwrap() error { + return e.Err +} + +// NewExitCodeError creates an ExitCodeError wrapping the given error +// with the specified exit code. +func NewExitCodeError(err error, code int) *ExitCodeError { + return &ExitCodeError{Err: err, ExitCode: code} +} diff --git a/cmd/entire/cli/errors_test.go b/cmd/entire/cli/errors_test.go new file mode 100644 index 000000000..0a041db5a --- /dev/null +++ b/cmd/entire/cli/errors_test.go @@ -0,0 +1,154 @@ +package cli + +import ( + "errors" + "fmt" + "testing" +) + +func TestSilentError(t *testing.T) { + t.Parallel() + + t.Run("wraps error message", func(t *testing.T) { + t.Parallel() + + inner := errors.New("something went wrong") + silent := NewSilentError(inner) + + if silent.Error() != "something went wrong" { + t.Errorf("expected %q, got %q", "something went wrong", silent.Error()) + } + }) + + t.Run("unwraps to inner error", func(t *testing.T) { + t.Parallel() + + inner := errors.New("inner error") + silent := NewSilentError(inner) + + if !errors.Is(silent, inner) { + t.Error("expected Unwrap to return inner error") + } + }) + + t.Run("detectable with errors.As", func(t *testing.T) { + t.Parallel() + + inner := errors.New("test") + silent := NewSilentError(inner) + + var target *SilentError + if !errors.As(silent, &target) { + t.Error("expected errors.As to find SilentError") + } + }) +} + +func TestExitCodeError(t *testing.T) { + t.Parallel() + + t.Run("wraps error message", func(t *testing.T) { + t.Parallel() + + inner := errors.New("command failed") + exitErr := NewExitCodeError(inner, 2) + + if exitErr.Error() != "command failed" { + t.Errorf("expected %q, got %q", "command failed", exitErr.Error()) + } + }) + + t.Run("stores exit code", func(t *testing.T) { + t.Parallel() + + inner := errors.New("test") + exitErr := NewExitCodeError(inner, 42) + + if exitErr.ExitCode != 42 { + t.Errorf("expected exit code 42, got %d", exitErr.ExitCode) + } + }) + + t.Run("unwraps to inner error", func(t *testing.T) { + t.Parallel() + + inner := errors.New("inner error") + exitErr := NewExitCodeError(inner, 1) + + if !errors.Is(exitErr, inner) { + t.Error("expected Unwrap to return inner error") + } + }) + + t.Run("detectable with errors.As", func(t *testing.T) { + t.Parallel() + + inner := errors.New("test") + exitErr := NewExitCodeError(inner, 3) + + var target *ExitCodeError + if !errors.As(exitErr, &target) { + t.Error("expected errors.As to find ExitCodeError") + } + + if target.ExitCode != 3 { + t.Errorf("expected exit code 3, got %d", target.ExitCode) + } + }) + + t.Run("detectable when wrapped by SilentError", func(t *testing.T) { + t.Parallel() + + inner := errors.New("already printed") + exitErr := NewExitCodeError(inner, 5) + silent := NewSilentError(exitErr) + + var target *ExitCodeError + if !errors.As(silent, &target) { + t.Error("expected errors.As to find ExitCodeError through SilentError") + } + + if target.ExitCode != 5 { + t.Errorf("expected exit code 5, got %d", target.ExitCode) + } + }) + + t.Run("detectable when wrapping SilentError", func(t *testing.T) { + t.Parallel() + + inner := errors.New("already printed") + silent := NewSilentError(inner) + exitErr := NewExitCodeError(silent, 7) + + var silentTarget *SilentError + if !errors.As(exitErr, &silentTarget) { + t.Error("expected errors.As to find SilentError through ExitCodeError") + } + + var exitTarget *ExitCodeError + if !errors.As(exitErr, &exitTarget) { + t.Error("expected errors.As to find ExitCodeError") + } + + if exitTarget.ExitCode != 7 { + t.Errorf("expected exit code 7, got %d", exitTarget.ExitCode) + } + }) + + t.Run("works with fmt.Errorf wrapping", func(t *testing.T) { + t.Parallel() + + inner := errors.New("root cause") + exitErr := NewExitCodeError(inner, 128) + wrapped := fmt.Errorf("command failed: %w", exitErr) + + var target *ExitCodeError + if !errors.As(wrapped, &target) { + t.Error("expected errors.As to find ExitCodeError through fmt.Errorf wrapping") + } + + if target.ExitCode != 128 { + t.Errorf("expected exit code 128, got %d", target.ExitCode) + } + }) +} diff --git a/cmd/entire/main.go b/cmd/entire/main.go index caad2ea9d..9b20de8d2 100644 --- a/cmd/entire/main.go +++ b/cmd/entire/main.go @@ -42,6 +42,12 @@ func main() { } cancel() + + var exitCodeErr *cli.ExitCodeError + if errors.As(err, &exitCodeErr) { + os.Exit(exitCodeErr.ExitCode) + } + os.Exit(1) } cancel() // Cleanup on successful exit