refactor : Refactor error handling to use dry/usefulerror (#283)

* update the go.sum

Signed-off-by: DivyanshuVortex <divyanshuchandra9027@gmail.com>

* migrate most of the files to dry errors

Signed-off-by: DivyanshuVortex <divyanshuchandra9027@gmail.com>

* update the rest of the files

Signed-off-by: DivyanshuVortex <divyanshuchandra9027@gmail.com>

* fixs the review comments

Signed-off-by: DivyanshuVortex <divyanshuchandra9027@gmail.com>

* chores

Signed-off-by: DivyanshuVortex <divyanshuchandra9027@gmail.com>

---------

Signed-off-by: DivyanshuVortex <divyanshuchandra9027@gmail.com>
Co-authored-by: Abhisek Datta <abhisek.datta@gmail.com>
This commit is contained in:
Divyanshu
2026-05-24 12:22:19 +05:30
committed by GitHub
co-authored by Abhisek Datta
parent e5fe0df82e
commit 20e01d5cae
33 changed files with 234 additions and 639 deletions
+2 -2
View File
@@ -15,7 +15,7 @@ import (
"github.com/safedep/pmg/packagemanager"
"github.com/safedep/pmg/sandbox"
"github.com/safedep/pmg/sandbox/executor"
"github.com/safedep/pmg/usefulerror"
"github.com/safedep/dry/usefulerror"
)
type ExecutionMode int
@@ -139,7 +139,7 @@ func runPTY(
beforeWait func(*PTYRuntime) error,
) error {
if !result.ShouldRun() {
return usefulerror.Useful().
return usefulerror.NewUsefulError().
Wrap(fmt.Errorf("sandbox not supported for PTY sessions")).
WithHumanError("Sandbox executed command cannot be used with PTY session. Please use non-interactive TTY mode instead.")
}
+2 -2
View File
@@ -5,7 +5,7 @@ import (
"os"
"github.com/safedep/dry/log"
"github.com/safedep/pmg/usefulerror"
"github.com/safedep/pmg/errcodes"
)
// ErrorExit prints a minimal, clean error message and exits with a non-zero status code.
@@ -23,7 +23,7 @@ func ErrorExitWithCode(err error, code int) {
// Use help as hint, but for unknown errors show bug report link
hint := usefulErr.Help()
if usefulErr.Code() == usefulerror.ErrCodeUnknown {
if usefulErr.Code() == errcodes.Unknown {
hint = "Report this issue: https://github.com/safedep/pmg/issues/new?labels=bug"
}
+31 -25
View File
@@ -11,7 +11,8 @@ import (
"os/exec"
"strings"
"github.com/safedep/pmg/usefulerror"
"github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/errcodes"
)
// errorMatcher defines how to detect and convert a specific error type
@@ -35,8 +36,8 @@ var errorMatchers = []errorMatcher{
humanError = fmt.Sprintf("File or directory not found: %s", path)
}
return usefulerror.Useful().
WithCode(usefulerror.ErrCodeNotFound).
return usefulerror.NewUsefulError().
WithCode(errcodes.NotFound).
WithHumanError(humanError).
WithHelp("Check if the path exists").
WithAdditionalHelp("Use 'ls' to check directory contents").
@@ -54,8 +55,8 @@ var errorMatchers = []errorMatcher{
if path != "" {
humanError = fmt.Sprintf("Permission denied: %s", path)
}
return usefulerror.Useful().
WithCode(usefulerror.ErrCodePermissionDenied).
return usefulerror.NewUsefulError().
WithCode(errcodes.PermissionDenied).
WithHumanError(humanError).
WithHelp("Check permissions or use sudo").
WithAdditionalHelp("Use 'ls -la' to check permissions").
@@ -72,8 +73,8 @@ var errorMatchers = []errorMatcher{
var exitErr *exec.ExitError
errors.As(err, &exitErr)
exitCode := exitErr.ExitCode()
return usefulerror.Useful().
WithCode(usefulerror.ErrCodeLifecycle).
return usefulerror.NewUsefulError().
WithCode(errcodes.Lifecycle).
WithHumanError(fmt.Sprintf("Command failed with exit code %d", exitCode)).
WithHelp("Check command output above").
Wrap(err)
@@ -85,8 +86,8 @@ var errorMatchers = []errorMatcher{
return errors.Is(err, context.DeadlineExceeded)
},
convert: func(err error) usefulerror.UsefulError {
return usefulerror.Useful().
WithCode(usefulerror.ErrCodeTimeout).
return usefulerror.NewUsefulError().
WithCode(errcodes.Timeout).
WithHumanError("Operation timed out").
WithHelp("Try again or check your network").
WithAdditionalHelp("Consider increasing timeout or retry later").
@@ -99,8 +100,8 @@ var errorMatchers = []errorMatcher{
return errors.Is(err, context.Canceled)
},
convert: func(err error) usefulerror.UsefulError {
return usefulerror.Useful().
WithCode(usefulerror.ErrCodeCanceled).
return usefulerror.NewUsefulError().
WithCode(errcodes.Canceled).
WithHumanError("Operation was canceled").
Wrap(err)
},
@@ -121,15 +122,15 @@ var errorMatchers = []errorMatcher{
convert: func(err error) usefulerror.UsefulError {
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() {
return usefulerror.Useful().
WithCode(usefulerror.ErrCodeTimeout).
return usefulerror.NewUsefulError().
WithCode(errcodes.Timeout).
WithHumanError("Network request timed out").
WithHelp("Check your internet connection").
WithAdditionalHelp("Consider increasing timeout or retry later").
Wrap(err)
}
return usefulerror.Useful().
WithCode(usefulerror.ErrCodeNetwork).
return usefulerror.NewUsefulError().
WithCode(errcodes.Network).
WithHumanError("Network error occurred").
WithHelp("Check your internet connection").
WithAdditionalHelp("The package registry may be temporarily unavailable").
@@ -142,8 +143,8 @@ var errorMatchers = []errorMatcher{
return errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF)
},
convert: func(err error) usefulerror.UsefulError {
return usefulerror.Useful().
WithCode(usefulerror.ErrCodeUnexpectedEOF).
return usefulerror.NewUsefulError().
WithCode(errcodes.UnexpectedEOF).
WithHumanError("Unexpected end of data").
WithHelp("Retry the download").
WithAdditionalHelp("This may indicate network instability").
@@ -152,6 +153,17 @@ var errorMatchers = []errorMatcher{
},
}
func init() {
usefulerror.RegisterErrorConverter("pmg-ui-converter", func(err error) (usefulerror.UsefulError, bool) {
for _, matcher := range errorMatchers {
if matcher.match(err) {
return matcher.convert(err), true
}
}
return nil, false
})
}
// convertToUsefulError attempts to convert a regular error to a UsefulError
// by analyzing the error chain for known error types.
// Returns the original error wrapped in a generic UsefulError if no specific match is found.
@@ -164,14 +176,8 @@ func convertToUsefulError(err error) usefulerror.UsefulError {
return ue
}
for _, matcher := range errorMatchers {
if matcher.match(err) {
return matcher.convert(err)
}
}
return usefulerror.Useful().
WithCode(usefulerror.ErrCodeUnknown).
return usefulerror.NewUsefulError().
WithCode(errcodes.Unknown).
WithHumanError(extractRootCause(err)).
WithHelp("An unexpected error occurred.").
Wrap(err)
+19 -19
View File
@@ -9,11 +9,12 @@ import (
"os"
"testing"
"github.com/safedep/pmg/usefulerror"
"github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/errcodes"
"github.com/stretchr/testify/assert"
)
func Test_convertToUsefulError(t *testing.T) {
func Test_ErrorConverters(t *testing.T) {
tests := []struct {
name string
inputError error
@@ -24,60 +25,58 @@ func Test_convertToUsefulError(t *testing.T) {
}{
{
name: "AlreadyUseful",
inputError: usefulerror.Useful().
inputError: usefulerror.NewUsefulError().
WithCode("CUSTOM").
WithHumanError("Already useful").
Msg("test"),
WithMsg("test"),
wantCode: "CUSTOM",
wantHumanError: "Already useful",
},
{
name: "FileNotExist",
inputError: &fs.PathError{Op: "open", Path: "/nonexistent/file.txt", Err: os.ErrNotExist},
wantCode: usefulerror.ErrCodeNotFound,
wantCode: errcodes.NotFound,
wantContains: "/nonexistent/file.txt",
},
{
name: "PermissionDenied",
inputError: &fs.PathError{Op: "open", Path: "/root/secret", Err: os.ErrPermission},
wantCode: usefulerror.ErrCodePermissionDenied,
wantCode: errcodes.PermissionDenied,
wantContains: "/root/secret",
},
{
name: "ContextTimeout",
inputError: context.DeadlineExceeded,
wantCode: usefulerror.ErrCodeTimeout,
wantCode: errcodes.Timeout,
wantContains: "timed out",
},
{
name: "ContextCanceled",
inputError: context.Canceled,
wantCode: usefulerror.ErrCodeCanceled,
wantCode: errcodes.Canceled,
wantContains: "canceled",
},
{
name: "UnexpectedEOF",
inputError: io.ErrUnexpectedEOF,
wantCode: usefulerror.ErrCodeUnexpectedEOF,
wantCode: errcodes.UnexpectedEOF,
},
{
name: "WrappedError",
inputError: fmt.Errorf("failed to read config: %w", os.ErrNotExist),
wantCode: usefulerror.ErrCodeNotFound,
wantCode: errcodes.NotFound,
},
{
name: "UnknownError",
inputError: errors.New("some unknown error"),
wantCode: usefulerror.ErrCodeUnknown,
wantHumanError: "some unknown error",
name: "UnknownError",
inputError: errors.New("some unknown error"),
wantNil: true,
},
{
name: "UnknownWrappedError",
inputError: fmt.Errorf("more context: %w",
fmt.Errorf("outer context: %w",
errors.New("root cause error"))),
wantCode: usefulerror.ErrCodeUnknown,
wantHumanError: "root cause error",
wantNil: true,
},
{
name: "Nil",
@@ -87,19 +86,20 @@ func Test_convertToUsefulError(t *testing.T) {
{
name: "NetworkErrorMessage",
inputError: errors.New("connection refused"),
wantCode: usefulerror.ErrCodeNetwork,
wantCode: errcodes.Network,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := convertToUsefulError(tt.inputError)
result, ok := usefulerror.AsUsefulError(tt.inputError)
if tt.wantNil {
assert.Nil(t, result)
assert.False(t, ok)
return
}
assert.True(t, ok)
assert.NotNil(t, result)
assert.Equal(t, tt.wantCode, result.Code())