Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions cmd/entire/cli/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -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}
}
154 changes: 154 additions & 0 deletions cmd/entire/cli/errors_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
6 changes: 6 additions & 0 deletions cmd/entire/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down