chore: Standardise Error Codes (#286)

* fix: Misc error handling fixes

* fix: Sandbox error translation
This commit is contained in:
Abhisek Datta
2026-05-24 12:46:06 +05:30
committed by GitHub
parent 20e01d5cae
commit 219d743b80
25 changed files with 162 additions and 47 deletions
+1 -1
View File
@@ -3,9 +3,9 @@ package cloud
import ( import (
"github.com/safedep/dry/cloud" "github.com/safedep/dry/cloud"
"github.com/safedep/dry/log" "github.com/safedep/dry/log"
"github.com/safedep/pmg/internal/ui"
"github.com/safedep/dry/usefulerror" "github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/errcodes" "github.com/safedep/pmg/errcodes"
"github.com/safedep/pmg/internal/ui"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
+1 -1
View File
@@ -3,9 +3,9 @@ package cloud
import ( import (
"github.com/safedep/dry/cloud" "github.com/safedep/dry/cloud"
"github.com/safedep/dry/log" "github.com/safedep/dry/log"
"github.com/safedep/pmg/internal/ui"
"github.com/safedep/dry/usefulerror" "github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/errcodes" "github.com/safedep/pmg/errcodes"
"github.com/safedep/pmg/internal/ui"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
+2 -2
View File
@@ -5,12 +5,12 @@ import (
"time" "time"
"github.com/safedep/dry/log" "github.com/safedep/dry/log"
"github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/config" "github.com/safedep/pmg/config"
"github.com/safedep/pmg/errcodes"
"github.com/safedep/pmg/internal/analytics" "github.com/safedep/pmg/internal/analytics"
"github.com/safedep/pmg/internal/audit" "github.com/safedep/pmg/internal/audit"
"github.com/safedep/pmg/internal/ui" "github.com/safedep/pmg/internal/ui"
"github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/errcodes"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
+2 -2
View File
@@ -10,10 +10,10 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
pmgsandbox "github.com/safedep/pmg/sandbox"
"github.com/safedep/pmg/sandbox/platform"
"github.com/safedep/dry/usefulerror" "github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/errcodes" "github.com/safedep/pmg/errcodes"
pmgsandbox "github.com/safedep/pmg/sandbox"
"github.com/safedep/pmg/sandbox/platform"
) )
// stubProbe is a minimal probe used by tests. // stubProbe is a minimal probe used by tests.
+2 -2
View File
@@ -8,11 +8,11 @@ import (
"strings" "strings"
"time" "time"
"github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/config" "github.com/safedep/pmg/config"
"github.com/safedep/pmg/errcodes"
"github.com/safedep/pmg/internal/ui" "github.com/safedep/pmg/internal/ui"
pmgsandbox "github.com/safedep/pmg/sandbox" pmgsandbox "github.com/safedep/pmg/sandbox"
"github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/errcodes"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
+1 -1
View File
@@ -12,9 +12,9 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
pmgsandbox "github.com/safedep/pmg/sandbox"
"github.com/safedep/dry/usefulerror" "github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/errcodes" "github.com/safedep/pmg/errcodes"
pmgsandbox "github.com/safedep/pmg/sandbox"
) )
func sampleReport() *pmgsandbox.ViolationReport { func sampleReport() *pmgsandbox.ViolationReport {
+12 -4
View File
@@ -9,10 +9,10 @@ import (
"regexp" "regexp"
"strings" "strings"
"github.com/safedep/pmg/internal/ui"
pmgsandbox "github.com/safedep/pmg/sandbox"
"github.com/safedep/dry/usefulerror" "github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/errcodes" "github.com/safedep/pmg/errcodes"
"github.com/safedep/pmg/internal/ui"
pmgsandbox "github.com/safedep/pmg/sandbox"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -70,7 +70,7 @@ func wrapUseful(err error, code, help string) error {
if err == nil { if err == nil {
return nil return nil
} }
if _, ok := usefulerror.AsUsefulError(err); ok { if hasUsefulError(err) {
return err return err
} }
return usefulerror.NewUsefulError(). return usefulerror.NewUsefulError().
@@ -84,7 +84,7 @@ func profileLoadError(err error) error {
if err == nil { if err == nil {
return nil return nil
} }
if _, ok := usefulerror.AsUsefulError(err); ok { if hasUsefulError(err) {
return err return err
} }
switch { switch {
@@ -99,6 +99,14 @@ func profileLoadError(err error) error {
"Failed to load the sandbox profile. Run with --verbose for the underlying cause.") "Failed to load the sandbox profile. Run with --verbose for the underlying cause.")
} }
func hasUsefulError(err error) bool {
// AsUsefulError also runs global converters for plain errors like
// fs.ErrPermission. Contextual wrappers must only skip errors that already
// carry UsefulError details, otherwise generic converters hide command help.
var usefulErr usefulerror.UsefulError
return errors.As(err, &usefulErr)
}
func registryInitError(err error) error { func registryInitError(err error) error {
return wrapUseful(err, ioErrorCode(err, errcodes.Unknown), return wrapUseful(err, ioErrorCode(err, errcodes.Unknown),
"Failed to initialise the sandbox profile registry. Run with --verbose for details.") "Failed to initialise the sandbox profile registry. Run with --verbose for details.")
+44
View File
@@ -0,0 +1,44 @@
package sandbox
import (
"errors"
"fmt"
"io/fs"
"testing"
"github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/errcodes"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestWrapUsefulPreservesContextForConvertibleErrors(t *testing.T) {
help := "Could not create the user profile directory. Check filesystem permissions for /var/root/pmg-repro/sandbox/profiles."
err := wrapUseful(
fmt.Errorf("failed to create user profile directory /var/root/pmg-repro/sandbox/profiles: %w", fs.ErrPermission),
errcodes.PermissionDenied,
help,
)
usefulErr, ok := usefulerror.AsUsefulError(err)
require.True(t, ok)
assert.Equal(t, errcodes.PermissionDenied, usefulErr.Code())
assert.Equal(t, help, usefulErr.Help())
assert.Contains(t, usefulErr.HumanError(), "failed to create user profile directory")
}
func TestWrapUsefulLeavesExistingUsefulErrorsUnchanged(t *testing.T) {
original := usefulerror.NewUsefulError().
WithCode(errcodes.InvalidArgument).
WithHumanError("already classified").
WithHelp("existing help").
Wrap(errors.New("root"))
err := wrapUseful(original, errcodes.PermissionDenied, "new help")
assert.Same(t, original, err)
usefulErr, ok := usefulerror.AsUsefulError(err)
require.True(t, ok)
assert.Equal(t, errcodes.InvalidArgument, usefulErr.Code())
assert.Equal(t, "existing help", usefulErr.Help())
}
+1 -1
View File
@@ -7,9 +7,9 @@ import (
"strings" "strings"
"github.com/pmezard/go-difflib/difflib" "github.com/pmezard/go-difflib/difflib"
"github.com/safedep/pmg/errcodes"
pmgsandbox "github.com/safedep/pmg/sandbox" pmgsandbox "github.com/safedep/pmg/sandbox"
"github.com/safedep/pmg/sandbox/platform" "github.com/safedep/pmg/sandbox/platform"
"github.com/safedep/pmg/errcodes"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
+1 -1
View File
@@ -7,9 +7,9 @@ import (
"strings" "strings"
"testing" "testing"
pmgsandbox "github.com/safedep/pmg/sandbox"
"github.com/safedep/dry/usefulerror" "github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/errcodes" "github.com/safedep/pmg/errcodes"
pmgsandbox "github.com/safedep/pmg/sandbox"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
+1 -1
View File
@@ -10,9 +10,9 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
pmgsandbox "github.com/safedep/pmg/sandbox"
"github.com/safedep/dry/usefulerror" "github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/errcodes" "github.com/safedep/pmg/errcodes"
pmgsandbox "github.com/safedep/pmg/sandbox"
) )
func writeUserProfileLint(t *testing.T, dir, name, body string) string { func writeUserProfileLint(t *testing.T, dir, name, body string) string {
+1 -1
View File
@@ -5,9 +5,9 @@ import (
"io" "io"
"strings" "strings"
"github.com/safedep/pmg/errcodes"
"github.com/safedep/pmg/internal/ui" "github.com/safedep/pmg/internal/ui"
pmgsandbox "github.com/safedep/pmg/sandbox" pmgsandbox "github.com/safedep/pmg/sandbox"
"github.com/safedep/pmg/errcodes"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
+1 -1
View File
@@ -13,9 +13,9 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
pmgsandbox "github.com/safedep/pmg/sandbox"
"github.com/safedep/dry/usefulerror" "github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/errcodes" "github.com/safedep/pmg/errcodes"
pmgsandbox "github.com/safedep/pmg/sandbox"
) )
func newTestRegistry(t *testing.T, userDir string) registryFactory { func newTestRegistry(t *testing.T, userDir string) registryFactory {
+1 -1
View File
@@ -6,9 +6,9 @@ import (
"os" "os"
"github.com/safedep/dry/log" "github.com/safedep/dry/log"
"github.com/safedep/pmg/errcodes"
pmgsandbox "github.com/safedep/pmg/sandbox" pmgsandbox "github.com/safedep/pmg/sandbox"
"github.com/safedep/pmg/sandbox/platform" "github.com/safedep/pmg/sandbox/platform"
"github.com/safedep/pmg/errcodes"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
+2 -2
View File
@@ -6,10 +6,10 @@ import (
"io" "io"
"time" "time"
"github.com/safedep/pmg/internal/ui"
pmgsandbox "github.com/safedep/pmg/sandbox"
"github.com/safedep/dry/usefulerror" "github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/errcodes" "github.com/safedep/pmg/errcodes"
"github.com/safedep/pmg/internal/ui"
pmgsandbox "github.com/safedep/pmg/sandbox"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
+1 -1
View File
@@ -6,9 +6,9 @@ import (
"strings" "strings"
"testing" "testing"
pmgsandbox "github.com/safedep/pmg/sandbox"
"github.com/safedep/dry/usefulerror" "github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/errcodes" "github.com/safedep/pmg/errcodes"
pmgsandbox "github.com/safedep/pmg/sandbox"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
+3 -3
View File
@@ -12,8 +12,8 @@ import (
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1" packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
"github.com/safedep/dry/log" "github.com/safedep/dry/log"
"github.com/safedep/dry/utils"
"github.com/safedep/dry/usefulerror" "github.com/safedep/dry/usefulerror"
"github.com/safedep/dry/utils"
"github.com/safedep/pmg/errcodes" "github.com/safedep/pmg/errcodes"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
@@ -337,8 +337,8 @@ func DefaultConfig() RuntimeConfig {
}, },
}, },
Proxy: ProxyConfig{ Proxy: ProxyConfig{
Enabled: true, Enabled: true,
InstallOnly: false, InstallOnly: false,
SkipCommands: map[string][]string{}, SkipCommands: map[string][]string{},
}, },
}, },
+12 -3
View File
@@ -7,10 +7,19 @@ const (
Timeout = "Timeout" Timeout = "Timeout"
Canceled = "Canceled" Canceled = "Canceled"
UnexpectedEOF = "UnexpectedEOF" UnexpectedEOF = "UnexpectedEOF"
Unknown = "Unknown"
Lifecycle = "Lifecycle" Lifecycle = "Lifecycle"
Network = "Network" Network = "Network"
SandboxViolation = "SandboxViolation"
PackageManagerExecutionFailed = "PackageManagerExecutionFailed" PackageManagerExecutionFailed = "PackageManagerExecutionFailed"
BubblewrapNotFound = "bubblewrap_not_found" BubblewrapNotFound = "BubblewrapNotFound"
// Package manager error codes.
DependencyResolutionFailed = "DependencyResolutionFailed"
PackageParseFailed = "PackageParseFailed"
PackageAuthorNotFound = "PackageAuthorNotFound"
GitHubRateLimitExceeded = "GitHubRateLimitExceeded"
// Unknown mirrors the default code that dry/usefulerror returns for errors
// created without an explicit code, so unset and explicitly-unknown errors
// classify identically (e.g. the bug-report hint in ui.ErrorExit).
Unknown = "unknown"
) )
+3 -1
View File
@@ -10,12 +10,13 @@ import (
"sync" "sync"
"github.com/safedep/dry/log" "github.com/safedep/dry/log"
"github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/errcodes"
"github.com/safedep/pmg/internal/pty" "github.com/safedep/pmg/internal/pty"
"github.com/safedep/pmg/internal/shim" "github.com/safedep/pmg/internal/shim"
"github.com/safedep/pmg/packagemanager" "github.com/safedep/pmg/packagemanager"
"github.com/safedep/pmg/sandbox" "github.com/safedep/pmg/sandbox"
"github.com/safedep/pmg/sandbox/executor" "github.com/safedep/pmg/sandbox/executor"
"github.com/safedep/dry/usefulerror"
) )
type ExecutionMode int type ExecutionMode int
@@ -141,6 +142,7 @@ func runPTY(
if !result.ShouldRun() { if !result.ShouldRun() {
return usefulerror.NewUsefulError(). return usefulerror.NewUsefulError().
Wrap(fmt.Errorf("sandbox not supported for PTY sessions")). Wrap(fmt.Errorf("sandbox not supported for PTY sessions")).
WithCode(errcodes.InvalidArgument).
WithHumanError("Sandbox executed command cannot be used with PTY session. Please use non-interactive TTY mode instead.") WithHumanError("Sandbox executed command cannot be used with PTY session. Please use non-interactive TTY mode instead.")
} }
+60 -1
View File
@@ -76,7 +76,7 @@ func Test_ErrorConverters(t *testing.T) {
inputError: fmt.Errorf("more context: %w", inputError: fmt.Errorf("more context: %w",
fmt.Errorf("outer context: %w", fmt.Errorf("outer context: %w",
errors.New("root cause error"))), errors.New("root cause error"))),
wantNil: true, wantNil: true,
}, },
{ {
name: "Nil", name: "Nil",
@@ -114,6 +114,65 @@ func Test_ErrorConverters(t *testing.T) {
} }
} }
func Test_convertToUsefulError(t *testing.T) {
tests := []struct {
name string
inputError error
wantCode string
wantHumanError string
wantNil bool
}{
{
name: "Nil",
inputError: nil,
wantNil: true,
},
{
name: "AlreadyUseful",
inputError: usefulerror.NewUsefulError().
WithCode("CUSTOM").
WithHumanError("Already useful"),
wantCode: "CUSTOM",
wantHumanError: "Already useful",
},
{
name: "Converted",
inputError: &fs.PathError{Op: "open", Path: "/nonexistent/file.txt", Err: os.ErrNotExist},
wantCode: errcodes.NotFound,
wantHumanError: "File or directory not found: /nonexistent/file.txt",
},
{
name: "UnknownFallsBackToRootCause",
inputError: errors.New("some unknown error"),
wantCode: errcodes.Unknown,
wantHumanError: "some unknown error",
},
{
name: "UnknownWrappedExtractsRootCause",
inputError: fmt.Errorf("more context: %w",
fmt.Errorf("outer context: %w",
errors.New("root cause error"))),
wantCode: errcodes.Unknown,
wantHumanError: "root cause error",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := convertToUsefulError(tt.inputError)
if tt.wantNil {
assert.Nil(t, result)
return
}
assert.NotNil(t, result)
assert.Equal(t, tt.wantCode, result.Code())
assert.Equal(t, tt.wantHumanError, result.HumanError())
})
}
}
func TestExtractPathFromError(t *testing.T) { func TestExtractPathFromError(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
+4 -11
View File
@@ -5,13 +5,6 @@ import (
"github.com/safedep/pmg/errcodes" "github.com/safedep/pmg/errcodes"
) )
const (
errDependencyResolutionFailed = "DependencyResolutionFailed"
errPackageParseFailed = "PackageParseFailed"
errPackageAuthorNotFound = "PackageAuthorNotFound"
errGitHubRateLimitExceeded = "GitHubRateLimitExceeded"
)
var ( var (
ErrPackageNotFound = usefulerror.NewUsefulError(). ErrPackageNotFound = usefulerror.NewUsefulError().
WithCode(errcodes.NotFound). WithCode(errcodes.NotFound).
@@ -31,25 +24,25 @@ var (
WithMsg("failed to resolve package version") WithMsg("failed to resolve package version")
ErrFailedToResolveDependencies = usefulerror.NewUsefulError(). ErrFailedToResolveDependencies = usefulerror.NewUsefulError().
WithCode(errDependencyResolutionFailed). WithCode(errcodes.DependencyResolutionFailed).
WithHumanError("Failed to resolve dependencies."). WithHumanError("Failed to resolve dependencies.").
WithHelp("Check your network connection and try again."). WithHelp("Check your network connection and try again.").
WithMsg("failed to resolve dependencies") WithMsg("failed to resolve dependencies")
ErrFailedToParsePackage = usefulerror.NewUsefulError(). ErrFailedToParsePackage = usefulerror.NewUsefulError().
WithCode(errPackageParseFailed). WithCode(errcodes.PackageParseFailed).
WithHumanError("The package data could not be processed."). WithHumanError("The package data could not be processed.").
WithHelp("The package may be corrupted or in an unsupported format."). WithHelp("The package may be corrupted or in an unsupported format.").
WithMsg("failed to parse package") WithMsg("failed to parse package")
ErrAuthorNotFound = usefulerror.NewUsefulError(). ErrAuthorNotFound = usefulerror.NewUsefulError().
WithCode(errPackageAuthorNotFound). WithCode(errcodes.PackageAuthorNotFound).
WithHumanError("The package author information could not be found."). WithHumanError("The package author information could not be found.").
WithHelp("This may be due to incomplete package metadata or network issues."). WithHelp("This may be due to incomplete package metadata or network issues.").
WithMsg("author not found") WithMsg("author not found")
ErrGitHubRateLimitExceeded = usefulerror.NewUsefulError(). ErrGitHubRateLimitExceeded = usefulerror.NewUsefulError().
WithCode(errGitHubRateLimitExceeded). WithCode(errcodes.GitHubRateLimitExceeded).
WithHumanError("GitHub API rate limit has been exceeded."). WithHumanError("GitHub API rate limit has been exceeded.").
WithHelp("Wait for the rate limit to reset or configure authentication to increase your rate limit."). WithHelp("Wait for the rate limit to reset or configure authentication to increase your rate limit.").
WithMsg("github api rate limit exceeded") WithMsg("github api rate limit exceeded")
+2 -2
View File
@@ -7,13 +7,13 @@ import (
"path/filepath" "path/filepath"
"github.com/safedep/dry/log" "github.com/safedep/dry/log"
"github.com/safedep/dry/usefulerror"
"github.com/safedep/dry/utils" "github.com/safedep/dry/utils"
"github.com/safedep/pmg/config" "github.com/safedep/pmg/config"
"github.com/safedep/pmg/errcodes"
"github.com/safedep/pmg/internal/audit" "github.com/safedep/pmg/internal/audit"
"github.com/safedep/pmg/sandbox" "github.com/safedep/pmg/sandbox"
"github.com/safedep/pmg/sandbox/platform" "github.com/safedep/pmg/sandbox/platform"
"github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/errcodes"
) )
type applySandboxConfig struct { type applySandboxConfig struct {
+2 -2
View File
@@ -4,10 +4,10 @@ import (
"fmt" "fmt"
"github.com/safedep/dry/log" "github.com/safedep/dry/log"
"github.com/safedep/pmg/config"
"github.com/safedep/pmg/sandbox"
"github.com/safedep/dry/usefulerror" "github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/config"
"github.com/safedep/pmg/errcodes" "github.com/safedep/pmg/errcodes"
"github.com/safedep/pmg/sandbox"
) )
// WrapCommandExecutionError converts a package manager execution error into a // WrapCommandExecutionError converts a package manager execution error into a
+1 -1
View File
@@ -6,9 +6,9 @@ import (
"os/exec" "os/exec"
"testing" "testing"
"github.com/safedep/pmg/sandbox"
"github.com/safedep/dry/usefulerror" "github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/errcodes" "github.com/safedep/pmg/errcodes"
"github.com/safedep/pmg/sandbox"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )