Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Return original error type #194

Closed
wants to merge 8 commits into from
Closed
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
23 changes: 21 additions & 2 deletions command/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,23 @@ type Command interface {
Wait() error
}

// FormattedError ...
type FormattedError struct {
formattedErr error
originalCommandErr error
}

// Error returns the formatted error message. Does not include the original error message (`exit status 1`).
func (c *FormattedError) Error() string {
return c.formattedErr.Error()
}

// Unwrap is needed for errors.Is and errors.As to work correctly.
// It does not change errorutil.FormattedError's behavior, as it uses Unwrap() error (not []error).
func (c *FormattedError) Unwrap() []error {
return []error{c.originalCommandErr}
}

type command struct {
cmd *exec.Cmd
errorCollector *errorCollector
Expand Down Expand Up @@ -169,9 +186,11 @@ func (c command) wrapError(err error) error {
var exitErr *exec.ExitError
if errors.As(err, &exitErr) {
if c.errorCollector != nil && len(c.errorCollector.errorLines) > 0 {
return fmt.Errorf("command failed with exit status %d (%s): %w", exitErr.ExitCode(), c.PrintableCommandArgs(), errors.New(strings.Join(c.errorCollector.errorLines, "\n")))
formattedErr := fmt.Errorf("command failed with exit status %d (%s): %w", exitErr.ExitCode(), c.PrintableCommandArgs(), errors.New(strings.Join(c.errorCollector.errorLines, "\n")))
return &FormattedError{formattedErr: formattedErr, originalCommandErr: err}
}
return fmt.Errorf("command failed with exit status %d (%s): %w", exitErr.ExitCode(), c.PrintableCommandArgs(), errors.New("check the command's output for details"))
formattedErr := fmt.Errorf("command failed with exit status %d (%s): %w", exitErr.ExitCode(), c.PrintableCommandArgs(), errors.New("check the command's output for details"))
return &FormattedError{formattedErr: formattedErr, originalCommandErr: err}
}
return fmt.Errorf("executing command failed (%s): %w", c.PrintableCommandArgs(), err)
}
Expand Down
29 changes: 28 additions & 1 deletion command/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@ package command

import (
"bytes"
"errors"
"fmt"
"os/exec"
"strings"
"testing"

"github.com/bitrise-io/go-utils/v2/env"
"github.com/bitrise-io/go-utils/v2/errorutil"
"github.com/stretchr/testify/assert"
)

Expand Down Expand Up @@ -65,9 +67,14 @@ Error: fourth error`,
gotErrMsg = err.Error()
}
if gotErrMsg != tt.wantErr {
t.Errorf("command.Run() error = %v, wantErr %v", gotErrMsg, tt.wantErr)
t.Errorf("command.Run() error = \n%v\n, wantErr \n%v\n", gotErrMsg, tt.wantErr)
return
}

gotFormattedMsg := errorutil.FormattedError(err)
if gotFormattedMsg != tt.wantErr {
t.Errorf("FormattedError() error = \n%v\n, wantErr \n%v\n", gotFormattedMsg, tt.wantErr)
}
})
}
}
Expand Down Expand Up @@ -123,6 +130,18 @@ func TestRunCmdAndReturnExitCode(t *testing.T) {
t.Errorf("command.RunAndReturnExitCode() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.wantErr && tt.wantExitCode > 0 {
var exitErr *exec.ExitError

if ok := errors.As(err, &exitErr); !ok {
t.Errorf("command.RunAndReturnExitCode() did nor return ExitError type: %s", err)
return
}

if exitErr.ExitCode() != tt.wantExitCode {
t.Errorf("command.RunAndReturnExitCode() exit code = %v, want %v", exitErr.ExitCode(), tt.wantExitCode)
}
}
if gotExitCode != tt.wantExitCode {
t.Errorf("command.RunAndReturnExitCode() = %v, want %v", gotExitCode, tt.wantExitCode)
}
Expand Down Expand Up @@ -180,6 +199,10 @@ Error: second error`,
t.Errorf("command.Run() error = %v, wantErr %v", gotErrMsg, tt.wantErr)
return
}
gotFormattedMsg := errorutil.FormattedError(err)
if gotFormattedMsg != tt.wantErr {
t.Errorf("FormattedError() error = \n%v\n, wantErr \n%v\n", gotFormattedMsg, tt.wantErr)
}
})
}
}
Expand Down Expand Up @@ -236,6 +259,10 @@ Error: fourth error`,
t.Errorf("command.Run() error = %v, wantErr %v", gotErrMsg, tt.wantErr)
return
}
gotFormattedMsg := errorutil.FormattedError(err)
if gotFormattedMsg != tt.wantErr {
t.Errorf("FormattedError() error = \n%v\n, wantErr \n%v\n", gotFormattedMsg, tt.wantErr)
}
})
}
}
Expand Down
73 changes: 58 additions & 15 deletions errorutil/formatted_error.go
Original file line number Diff line number Diff line change
@@ -1,34 +1,73 @@
package errorutil

import (
"errors"
"strings"
)

// FormattedError ...
func FormattedError(err error) string {
var formatted string

i := -1
for {
i++
func unwrap(err error) []error {
switch x := err.(type) {
case interface{ Unwrap() []error }:
return x.Unwrap()
case interface{ Unwrap() error }:
return []error{x.Unwrap()}
default:
return nil
}
}

reason := err.Error()
func formattedError(err error, printDebugOnly bool, indent int) string {
debugErr, isDebugErr := err.(DebugError)
if isDebugErr {
err = debugErr.OriginalError()
}

if err = errors.Unwrap(err); err == nil {
formatted = appendError(formatted, reason, i, true)
return formatted
formatted := ""
reason := err.Error()
wrappedErrs := []error{}
if wrappedErrs = unwrap(err); len(wrappedErrs) == 0 {
if isDebugErr == printDebugOnly {
formatted = appendError(formatted, reason, indent, true)
}
return formatted
}

reason = strings.TrimSuffix(reason, err.Error())
reason = strings.TrimRight(reason, " ")
for i := len(wrappedErrs) - 1; i >= 0; i-- {
reason = strings.TrimSuffix(reason, wrappedErrs[i].Error())
reason = strings.TrimRight(reason, "\n ")
reason = strings.TrimSuffix(reason, ":")
}

formatted = appendError(formatted, reason, i, false)
if isDebugErr == printDebugOnly {
formatted += appendError(formatted, reason, indent, false)
}
if !printDebugOnly && isDebugErr { // skip children of debug errors
return formatted
}
if printDebugOnly && isDebugErr { // print children of debug errors
printDebugOnly = false
}
for _, wrappedErr := range wrappedErrs {
formatted += formattedError(wrappedErr, printDebugOnly, indent+1)
}

return formatted
}

// FormattedError ...
func FormattedError(err error) string {
return formattedError(err, false, 0)
}

// FormattedErrorInternalDebugInfo ...
func FormattedErrorInternalDebugInfo(err error) string {
return formattedError(err, true, 0)
}

func appendError(errorMessage, reason string, i int, last bool) string {
if reason == "" {
return ""
}

if i == 0 {
errorMessage = indentedReason(reason, i)
} else {
Expand All @@ -46,6 +85,10 @@ func appendError(errorMessage, reason string, i int, last bool) string {
func indentedReason(reason string, level int) string {
var lines []string
split := strings.Split(reason, "\n")
if len(split) == 1 && split[0] == "" {
split = []string{"[empty error string]"}
}

for _, line := range split {
line = strings.TrimLeft(line, " ")
line = strings.TrimRight(line, "\n")
Expand Down
28 changes: 27 additions & 1 deletion errorutil/formatted_error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,39 @@ func TestFormattedError(t *testing.T) {
return err
}, wantFormattedError: "fourth layer also failed: third layer also failed: second layer also failed: the magic has failed",
},
{
name: "Multiple wrapped errors in the same level",
error: func() error {
err := errors.New("the internal debug info")
err2 := fmt.Errorf("the description")
err = fmt.Errorf("third layer also failed: %w %w", err2, err)
err = fmt.Errorf("fourth layer also failed: %w", err)
return err
}, wantFormattedError: `fourth layer also failed:
third layer also failed:
the description
the internal debug info`,
},
{
name: "Multiple wrapped errors in the same level, debug info hidden from stack trace",
error: func() error {
err := NewInternalDebugError(errors.New("the internal debug info"))
err2 := fmt.Errorf("the description")
err = fmt.Errorf("third layer also failed: %w %w", err, err2)
err = fmt.Errorf("fourth layer also failed: %w", err)
return err
}, wantFormattedError: `fourth layer also failed:
third layer also failed:
the description`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
formatted := FormattedError(tt.error())

// require.Equal(t, tt.wantFormattedError, formatted)
if formatted != tt.wantFormattedError {
t.Errorf("got formatted error = %s, want %s", formatted, tt.wantFormattedError)
t.Errorf("got formatted error = \n%s\n, want \n%s", formatted, tt.wantFormattedError)
}
})
}
Expand Down
47 changes: 47 additions & 0 deletions errorutil/interal_debug_error.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package errorutil

import (
"errors"
)

// DebugError type errors will not be included in FormattedError output
// but due to Unwrap() any error that is wrapped by an InternalDebugError can be used with errors.As()
type DebugError interface {
Error() string
Unwrap() error
OriginalError() error
}

// JoinInternalDebugError ...
func JoinInternalDebugError(err error, debugErr error) error {
return errors.Join(err, NewInternalDebugError(debugErr))
}

// InternalDebugError allows to include an error in the error chain but do not print it.
// this allows replacing it with a more readable error message,
// while allowing code to check for the type of the error
type InternalDebugError struct {
originalErr error
}

// NewInternalDebugError ...
func NewInternalDebugError(originalErr error) error {
return &InternalDebugError{
originalErr: originalErr,
}
}

// Error ...
func (e *InternalDebugError) Error() string {
return "" // do not print this error as it is internal debug info
}

// Unwrap ...
func (e *InternalDebugError) Unwrap() error {
return e.originalErr
}

// OriginalError ...
func (e *InternalDebugError) OriginalError() error {
return e.originalErr
}