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
+22 -21
View File
@@ -4,7 +4,8 @@ import (
"github.com/safedep/dry/cloud"
"github.com/safedep/dry/log"
"github.com/safedep/pmg/internal/ui"
"github.com/safedep/pmg/usefulerror"
"github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/errcodes"
"github.com/spf13/cobra"
)
@@ -29,70 +30,70 @@ func runLogin(cmd *cobra.Command, args []string) error {
if loginFromEnv {
resolver, err := cloud.NewEnvCredentialResolver()
if err != nil {
ui.ErrorExit(usefulerror.Useful().
ui.ErrorExit(usefulerror.NewUsefulError().
Wrap(err).
WithCode(usefulerror.ErrCodeLifecycle).
WithCode(errcodes.Lifecycle).
WithHumanError("Failed to create environment credential resolver"))
}
creds, err := resolver.Resolve()
if err != nil {
ui.ErrorExit(usefulerror.Useful().
ui.ErrorExit(usefulerror.NewUsefulError().
Wrap(err).
WithCode(usefulerror.ErrCodeInvalidArgument).
WithCode(errcodes.InvalidArgument).
WithHumanError("Failed to resolve credentials from environment").
WithHelp("Set SAFEDEP_API_KEY and SAFEDEP_TENANT_ID environment variables"))
}
apiKey, err = creds.GetAPIKey()
if err != nil || apiKey == "" {
ui.ErrorExit(usefulerror.Useful().
WithCode(usefulerror.ErrCodeInvalidArgument).
ui.ErrorExit(usefulerror.NewUsefulError().
WithCode(errcodes.InvalidArgument).
WithHumanError("SAFEDEP_API_KEY environment variable is not set"))
}
tenantID, err = creds.GetTenantDomain()
if err != nil || tenantID == "" {
ui.ErrorExit(usefulerror.Useful().
WithCode(usefulerror.ErrCodeInvalidArgument).
ui.ErrorExit(usefulerror.NewUsefulError().
WithCode(errcodes.InvalidArgument).
WithHumanError("SAFEDEP_TENANT_ID environment variable is not set"))
}
} else {
var err error
tenantID, err = ui.PromptInput("Tenant ID: ")
if err != nil {
ui.ErrorExit(usefulerror.Useful().
ui.ErrorExit(usefulerror.NewUsefulError().
Wrap(err).
WithCode(usefulerror.ErrCodeLifecycle).
WithCode(errcodes.Lifecycle).
WithHumanError("Failed to read Tenant ID"))
}
if tenantID == "" {
ui.ErrorExit(usefulerror.Useful().
WithCode(usefulerror.ErrCodeInvalidArgument).
ui.ErrorExit(usefulerror.NewUsefulError().
WithCode(errcodes.InvalidArgument).
WithHumanError("Tenant ID cannot be empty"))
}
apiKey, err = ui.PromptSecret("API Key: ")
if err != nil {
ui.ErrorExit(usefulerror.Useful().
ui.ErrorExit(usefulerror.NewUsefulError().
Wrap(err).
WithCode(usefulerror.ErrCodeLifecycle).
WithCode(errcodes.Lifecycle).
WithHumanError("Failed to read API Key"))
}
if apiKey == "" {
ui.ErrorExit(usefulerror.Useful().
WithCode(usefulerror.ErrCodeInvalidArgument).
ui.ErrorExit(usefulerror.NewUsefulError().
WithCode(errcodes.InvalidArgument).
WithHumanError("API Key cannot be empty"))
}
}
store, err := cloud.NewKeychainCredentialStore()
if err != nil {
ui.ErrorExit(usefulerror.Useful().
ui.ErrorExit(usefulerror.NewUsefulError().
Wrap(err).
WithCode(usefulerror.ErrCodeLifecycle).
WithCode(errcodes.Lifecycle).
WithHumanError("Failed to initialize credential store").
WithHelp("Your system may not support secure credential storage"))
}
@@ -103,9 +104,9 @@ func runLogin(cmd *cobra.Command, args []string) error {
}()
if err := store.SaveAPIKeyCredential(apiKey, tenantID); err != nil {
ui.ErrorExit(usefulerror.Useful().
ui.ErrorExit(usefulerror.NewUsefulError().
Wrap(err).
WithCode(usefulerror.ErrCodeLifecycle).
WithCode(errcodes.Lifecycle).
WithHumanError("Failed to save credentials").
WithHelp("Your system may not support secure credential storage"))
}
+6 -5
View File
@@ -4,7 +4,8 @@ import (
"github.com/safedep/dry/cloud"
"github.com/safedep/dry/log"
"github.com/safedep/pmg/internal/ui"
"github.com/safedep/pmg/usefulerror"
"github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/errcodes"
"github.com/spf13/cobra"
)
@@ -19,9 +20,9 @@ func newLogoutCommand() *cobra.Command {
func runLogout(cmd *cobra.Command, args []string) error {
store, err := cloud.NewKeychainCredentialStore()
if err != nil {
ui.ErrorExit(usefulerror.Useful().
ui.ErrorExit(usefulerror.NewUsefulError().
Wrap(err).
WithCode(usefulerror.ErrCodeLifecycle).
WithCode(errcodes.Lifecycle).
WithHumanError("Failed to initialize credential store").
WithHelp("Your system may not support secure credential storage"))
}
@@ -32,9 +33,9 @@ func runLogout(cmd *cobra.Command, args []string) error {
}()
if err := store.Clear(); err != nil {
ui.ErrorExit(usefulerror.Useful().
ui.ErrorExit(usefulerror.NewUsefulError().
Wrap(err).
WithCode(usefulerror.ErrCodeLifecycle).
WithCode(errcodes.Lifecycle).
WithHumanError("Failed to clear credentials"))
}
+12 -11
View File
@@ -9,7 +9,8 @@ import (
"github.com/safedep/pmg/internal/analytics"
"github.com/safedep/pmg/internal/audit"
"github.com/safedep/pmg/internal/ui"
"github.com/safedep/pmg/usefulerror"
"github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/errcodes"
"github.com/spf13/cobra"
)
@@ -42,8 +43,8 @@ func runSync(cmd *cobra.Command, args []string) error {
}
if !cfg.Config.Cloud.Enabled {
ui.ErrorExit(usefulerror.Useful().
WithCode(usefulerror.ErrCodeLifecycle).
ui.ErrorExit(usefulerror.NewUsefulError().
WithCode(errcodes.Lifecycle).
WithHumanError("Cloud sync is not enabled").
WithHelp("Set 'cloud.enabled: true' in PMG config to enable cloud sync"))
}
@@ -54,15 +55,15 @@ func runSync(cmd *cobra.Command, args []string) error {
locked, err := lock.TryLockContext(lockCtx, 250*time.Millisecond)
if err != nil {
ui.ErrorExit(usefulerror.Useful().
ui.ErrorExit(usefulerror.NewUsefulError().
Wrap(err).
WithCode(usefulerror.ErrCodeLifecycle).
WithCode(errcodes.Lifecycle).
WithHumanError("Failed to acquire cloud sync lock").
WithHelp("Another sync may be in progress; try again shortly"))
}
if !locked {
ui.ErrorExit(usefulerror.Useful().
WithCode(usefulerror.ErrCodeLifecycle).
ui.ErrorExit(usefulerror.NewUsefulError().
WithCode(errcodes.Lifecycle).
WithHumanError("Another cloud sync is already in progress").
WithHelp("Wait for the in-progress sync to finish, then try again"))
}
@@ -77,9 +78,9 @@ func runSync(cmd *cobra.Command, args []string) error {
bundle, err := audit.NewSyncClientBundle(cfg)
if err != nil {
ui.ErrorExit(usefulerror.Useful().
ui.ErrorExit(usefulerror.NewUsefulError().
Wrap(err).
WithCode(usefulerror.ErrCodeLifecycle).
WithCode(errcodes.Lifecycle).
WithHumanError("Failed to initialize cloud sync client").
WithHelp("Run 'pmg cloud login' to store credentials, or set SAFEDEP_API_KEY and SAFEDEP_TENANT_ID environment variables"))
}
@@ -92,9 +93,9 @@ func runSync(cmd *cobra.Command, args []string) error {
synced, err := bundle.Sync(ctx)
recordLastSyncAttempt(cfg)
if err != nil {
ui.ErrorExit(usefulerror.Useful().
ui.ErrorExit(usefulerror.NewUsefulError().
Wrap(err).
WithCode(usefulerror.ErrCodeNetwork).
WithCode(errcodes.Network).
WithHumanError("Failed to sync events to SafeDep Cloud").
WithHelp("Check your network connectivity and ensure SafeDep Cloud is reachable").
WithAdditionalHelp("Override the cloud endpoint with SAFEDEP_CLOUD_DATA_ADDR if needed"))
+3 -2
View File
@@ -12,7 +12,8 @@ import (
pmgsandbox "github.com/safedep/pmg/sandbox"
"github.com/safedep/pmg/sandbox/platform"
"github.com/safedep/pmg/usefulerror"
"github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/errcodes"
)
// stubProbe is a minimal probe used by tests.
@@ -169,7 +170,7 @@ func TestRunDoctor_UnknownDriver(t *testing.T) {
assert.Contains(t, err.Error(), "unknown driver")
usefulErr, ok := usefulerror.AsUsefulError(err)
require.True(t, ok)
assert.Equal(t, usefulerror.ErrCodeInvalidArgument, usefulErr.Code())
assert.Equal(t, errcodes.InvalidArgument, usefulErr.Code())
}
func TestDoctorCommandRejectsUnexpectedArgsWithUsage(t *testing.T) {
+15 -14
View File
@@ -11,7 +11,8 @@ import (
"github.com/safedep/pmg/config"
"github.com/safedep/pmg/internal/ui"
pmgsandbox "github.com/safedep/pmg/sandbox"
"github.com/safedep/pmg/usefulerror"
"github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/errcodes"
"github.com/spf13/cobra"
)
@@ -67,7 +68,7 @@ func (e *explainFailError) ExitCode() int { return ExitCodeExplainFail }
func newExplainFailError(code, msg, help string) *explainFailError {
return &explainFailError{
UsefulError: usefulerror.Useful().
UsefulError: usefulerror.NewUsefulError().
WithCode(code).
WithHumanError(msg).
WithHelp(help).
@@ -80,7 +81,7 @@ func runExplain(out io.Writer, in io.Reader, args []string, opts *explainOptions
if len(args) == 1 && !stdinMode {
return newExplainFailError(
usefulerror.ErrCodeInvalidArgument,
errcodes.InvalidArgument,
fmt.Sprintf("unexpected argument %q (use --last or pipe JSON with `-`)", args[0]),
explainUsageHelp(),
)
@@ -88,7 +89,7 @@ func runExplain(out io.Writer, in io.Reader, args []string, opts *explainOptions
if opts.last && stdinMode {
return newExplainFailError(
usefulerror.ErrCodeInvalidArgument,
errcodes.InvalidArgument,
"--last and `-` are mutually exclusive",
explainUsageHelp(),
)
@@ -96,7 +97,7 @@ func runExplain(out io.Writer, in io.Reader, args []string, opts *explainOptions
if !opts.last && !stdinMode {
return newExplainFailError(
usefulerror.ErrCodeInvalidArgument,
errcodes.InvalidArgument,
"no input: pass --last to read the most recent cached violation, or pipe a violation record JSON on stdin with `-`",
explainUsageHelp(),
)
@@ -128,14 +129,14 @@ func readLatestFromCache(factory cacheFactory) (*pmgsandbox.ViolationCacheRecord
entry, err := cache.Latest()
if err != nil {
return nil, newExplainFailError(
usefulerror.ErrCodeUnknown,
errcodes.Unknown,
fmt.Sprintf("read cache: %v", err),
"Check the sandbox violation cache directory and retry.",
)
}
if entry == nil {
return nil, newExplainFailError(
usefulerror.ErrCodeNotFound,
errcodes.NotFound,
"no violations cached yet — run a sandboxed command first",
"Run a sandboxed package manager command first, then retry `pmg sandbox explain --last`.",
)
@@ -151,7 +152,7 @@ func readRecordFromStdin(in io.Reader) (*pmgsandbox.ViolationCacheRecord, error)
data, err := io.ReadAll(in)
if err != nil {
return nil, newExplainFailError(
usefulerror.ErrCodeInvalidArgument,
errcodes.InvalidArgument,
fmt.Sprintf("read stdin: %v", err),
explainUsageHelp(),
)
@@ -159,7 +160,7 @@ func readRecordFromStdin(in io.Reader) (*pmgsandbox.ViolationCacheRecord, error)
if len(strings.TrimSpace(string(data))) == 0 {
return nil, newExplainFailError(
usefulerror.ErrCodeInvalidArgument,
errcodes.InvalidArgument,
"stdin is empty: pipe a ViolationCacheRecord JSON document",
explainUsageHelp(),
)
@@ -168,7 +169,7 @@ func readRecordFromStdin(in io.Reader) (*pmgsandbox.ViolationCacheRecord, error)
var rec pmgsandbox.ViolationCacheRecord
if err := json.Unmarshal(data, &rec); err != nil {
return nil, newExplainFailError(
usefulerror.ErrCodeInvalidArgument,
errcodes.InvalidArgument,
fmt.Sprintf("parse stdin JSON: %v", err),
"Pipe a valid ViolationCacheRecord JSON document to `pmg sandbox explain -`.",
)
@@ -183,16 +184,16 @@ func readRecordFromStdin(in io.Reader) (*pmgsandbox.ViolationCacheRecord, error)
func validateViolationCacheRecord(rec *pmgsandbox.ViolationCacheRecord, source string) error {
if rec == nil {
return newExplainFailError(usefulerror.ErrCodeInvalidArgument, fmt.Sprintf("%s is empty", source), explainUsageHelp())
return newExplainFailError(errcodes.InvalidArgument, fmt.Sprintf("%s is empty", source), explainUsageHelp())
}
if rec.SchemaVersion == 0 {
return newExplainFailError(usefulerror.ErrCodeInvalidArgument, fmt.Sprintf("%s is missing schema_version", source), explainUsageHelp())
return newExplainFailError(errcodes.InvalidArgument, fmt.Sprintf("%s is missing schema_version", source), explainUsageHelp())
}
if rec.SchemaVersion != pmgsandbox.ViolationCacheSchemaVersion {
return newExplainFailError(usefulerror.ErrCodeInvalidArgument, fmt.Sprintf("unknown schema_version %d (expected %d)", rec.SchemaVersion, pmgsandbox.ViolationCacheSchemaVersion), explainUsageHelp())
return newExplainFailError(errcodes.InvalidArgument, fmt.Sprintf("unknown schema_version %d (expected %d)", rec.SchemaVersion, pmgsandbox.ViolationCacheSchemaVersion), explainUsageHelp())
}
if rec.Report == nil {
return newExplainFailError(usefulerror.ErrCodeInvalidArgument, fmt.Sprintf("%s is missing report", source), explainUsageHelp())
return newExplainFailError(errcodes.InvalidArgument, fmt.Sprintf("%s is missing report", source), explainUsageHelp())
}
return nil
+4 -3
View File
@@ -13,7 +13,8 @@ import (
"github.com/stretchr/testify/require"
pmgsandbox "github.com/safedep/pmg/sandbox"
"github.com/safedep/pmg/usefulerror"
"github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/errcodes"
)
func sampleReport() *pmgsandbox.ViolationReport {
@@ -67,7 +68,7 @@ func TestExplainLastEmptyCache(t *testing.T) {
assert.Equal(t, ExitCodeExplainFail, fe.ExitCode())
usefulErr, ok := usefulerror.AsUsefulError(err)
require.True(t, ok)
assert.Equal(t, usefulerror.ErrCodeNotFound, usefulErr.Code())
assert.Equal(t, errcodes.NotFound, usefulErr.Code())
assert.Empty(t, stdout)
assert.Contains(t, err.Error(), "no violations cached")
}
@@ -221,7 +222,7 @@ func TestExplainNoMode(t *testing.T) {
require.Error(t, err)
usefulErr, ok := usefulerror.AsUsefulError(err)
require.True(t, ok)
assert.Equal(t, usefulerror.ErrCodeInvalidArgument, usefulErr.Code())
assert.Equal(t, errcodes.InvalidArgument, usefulErr.Code())
assert.Contains(t, err.Error(), "no input")
assert.Contains(t, err.Error(), "--last")
}
+13 -12
View File
@@ -11,7 +11,8 @@ import (
"github.com/safedep/pmg/internal/ui"
pmgsandbox "github.com/safedep/pmg/sandbox"
"github.com/safedep/pmg/usefulerror"
"github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/errcodes"
"github.com/spf13/cobra"
)
@@ -48,16 +49,16 @@ func validateDriver(name string) error {
}
func invalidArgumentError(message, help string) error {
return usefulerror.Useful().
WithCode(usefulerror.ErrCodeInvalidArgument).
return usefulerror.NewUsefulError().
WithCode(errcodes.InvalidArgument).
WithHumanError(message).
WithHelp(help).
Wrap(errors.New(message))
}
func notFoundError(message, help string) error {
return usefulerror.Useful().
WithCode(usefulerror.ErrCodeNotFound).
return usefulerror.NewUsefulError().
WithCode(errcodes.NotFound).
WithHumanError(message).
WithHelp(help).
Wrap(errors.New(message))
@@ -72,7 +73,7 @@ func wrapUseful(err error, code, help string) error {
if _, ok := usefulerror.AsUsefulError(err); ok {
return err
}
return usefulerror.Useful().
return usefulerror.NewUsefulError().
WithCode(code).
WithHumanError(err.Error()).
WithHelp(help).
@@ -88,27 +89,27 @@ func profileLoadError(err error) error {
}
switch {
case errors.Is(err, pmgsandbox.ErrProfileNotFound):
return wrapUseful(err, usefulerror.ErrCodeNotFound,
return wrapUseful(err, errcodes.NotFound,
"Use `pmg sandbox profile list` to see available profiles, or pass an existing profile YAML path.")
case errors.Is(err, pmgsandbox.ErrProfileInvalid):
return wrapUseful(err, usefulerror.ErrCodeInvalidArgument,
return wrapUseful(err, errcodes.InvalidArgument,
"Check the profile YAML for syntax/schema issues and verify any 'inherits:' parent name.")
}
return wrapUseful(err, usefulerror.ErrCodeUnknown,
return wrapUseful(err, errcodes.Unknown,
"Failed to load the sandbox profile. Run with --verbose for the underlying cause.")
}
func registryInitError(err error) error {
return wrapUseful(err, ioErrorCode(err, usefulerror.ErrCodeUnknown),
return wrapUseful(err, ioErrorCode(err, errcodes.Unknown),
"Failed to initialise the sandbox profile registry. Run with --verbose for details.")
}
func ioErrorCode(err error, fallback string) string {
switch {
case errors.Is(err, fs.ErrPermission):
return usefulerror.ErrCodePermissionDenied
return errcodes.PermissionDenied
case errors.Is(err, fs.ErrNotExist):
return usefulerror.ErrCodeNotFound
return errcodes.NotFound
}
return fallback
}
+7 -7
View File
@@ -9,7 +9,7 @@ import (
"github.com/pmezard/go-difflib/difflib"
pmgsandbox "github.com/safedep/pmg/sandbox"
"github.com/safedep/pmg/sandbox/platform"
"github.com/safedep/pmg/usefulerror"
"github.com/safedep/pmg/errcodes"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)
@@ -83,7 +83,7 @@ func runProfileDiff(out io.Writer, errOut io.Writer, nameA, nameB string, opts *
if bytes.Equal(dataA, dataB) {
if _, err := fmt.Fprintln(errOut, "profiles are identical"); err != nil {
return &diffOpError{err: wrapUseful(err, usefulerror.ErrCodeUnknown,
return &diffOpError{err: wrapUseful(err, errcodes.Unknown,
"Failed to write diff output. Check that stderr is writable.")}
}
return nil
@@ -99,18 +99,18 @@ func runProfileDiff(out io.Writer, errOut io.Writer, nameA, nameB string, opts *
text, err := difflib.GetUnifiedDiffString(diff)
if err != nil {
return &diffOpError{err: wrapUseful(fmt.Errorf("failed to render diff: %w", err),
usefulerror.ErrCodeUnknown,
errcodes.Unknown,
"Could not render the unified diff. Re-run with --verbose for the underlying cause.")}
}
if _, err := io.WriteString(out, text); err != nil {
return &diffOpError{err: wrapUseful(err, usefulerror.ErrCodeUnknown,
return &diffOpError{err: wrapUseful(err, errcodes.Unknown,
"Failed to write diff output. Check that stdout is writable.")}
}
if !strings.HasSuffix(text, "\n") {
if _, err := fmt.Fprintln(out); err != nil {
return &diffOpError{err: wrapUseful(err, usefulerror.ErrCodeUnknown,
return &diffOpError{err: wrapUseful(err, errcodes.Unknown,
"Failed to write diff output. Check that stdout is writable.")}
}
}
@@ -130,7 +130,7 @@ func materialize(registry pmgsandbox.ProfileRegistry, name string, opts *profile
if opts.driver != "" {
rendered, err := platform.Render(pmgsandbox.DriverName(opts.driver), policy)
if err != nil {
return nil, wrapUseful(err, usefulerror.ErrCodeInvalidArgument,
return nil, wrapUseful(err, errcodes.InvalidArgument,
"Could not render the policy for the requested driver. Verify the driver is supported on this host.")
}
return rendered, nil
@@ -142,7 +142,7 @@ func materialize(registry pmgsandbox.ProfileRegistry, name string, opts *profile
data, err := yaml.Marshal(policy)
if err != nil {
return nil, wrapUseful(fmt.Errorf("failed to marshal resolved policy %s: %w", name, err),
usefulerror.ErrCodeUnknown,
errcodes.Unknown,
"Failed to marshal the resolved policy to YAML. Re-run with --verbose for the underlying cause.")
}
return data, nil
+4 -3
View File
@@ -8,7 +8,8 @@ import (
"strings"
"testing"
"github.com/safedep/pmg/usefulerror"
"github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/errcodes"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -99,7 +100,7 @@ func TestProfileDiffUnknownProfile(t *testing.T) {
assert.Contains(t, err.Error(), "no-such-profile-xyz")
usefulErr, ok := usefulerror.AsUsefulError(err)
require.True(t, ok, "diff error should expose a UsefulError through Unwrap")
assert.Equal(t, usefulerror.ErrCodeNotFound, usefulErr.Code())
assert.Equal(t, errcodes.NotFound, usefulErr.Code())
}
func TestProfileDiffUnknownDriver(t *testing.T) {
@@ -113,7 +114,7 @@ func TestProfileDiffUnknownDriver(t *testing.T) {
assert.Contains(t, err.Error(), "unknown driver")
usefulErr, ok := usefulerror.AsUsefulError(err)
require.True(t, ok, "diff error should expose a UsefulError through Unwrap")
assert.Equal(t, usefulerror.ErrCodeInvalidArgument, usefulErr.Code())
assert.Equal(t, errcodes.InvalidArgument, usefulErr.Code())
}
func TestProfileDiffMissingProfileShowsUsage(t *testing.T) {
+4 -4
View File
@@ -8,7 +8,7 @@ import (
"regexp"
"strings"
"github.com/safedep/pmg/usefulerror"
"github.com/safedep/pmg/errcodes"
"github.com/spf13/cobra"
)
@@ -95,13 +95,13 @@ func runProfileInit(out io.Writer, name string, opts *profileInitOptions, factor
)
} else if !os.IsNotExist(err) {
return wrapUseful(fmt.Errorf("failed to stat %s: %w", target, err),
ioErrorCode(err, usefulerror.ErrCodeUnknown),
ioErrorCode(err, errcodes.Unknown),
"Could not stat the target profile path. Check the user profile directory permissions.")
}
if err := os.MkdirAll(userDir, 0o755); err != nil {
return wrapUseful(fmt.Errorf("failed to create user profile directory %s: %w", userDir, err),
ioErrorCode(err, usefulerror.ErrCodePermissionDenied),
ioErrorCode(err, errcodes.PermissionDenied),
"Could not create the user profile directory. Check filesystem permissions for "+userDir+".")
}
@@ -109,7 +109,7 @@ func runProfileInit(out io.Writer, name string, opts *profileInitOptions, factor
if err := os.WriteFile(target, []byte(content), 0o644); err != nil {
return wrapUseful(fmt.Errorf("failed to write %s: %w", target, err),
ioErrorCode(err, usefulerror.ErrCodePermissionDenied),
ioErrorCode(err, errcodes.PermissionDenied),
"Could not write the scaffolded profile. Check filesystem permissions for "+target+".")
}
+5 -4
View File
@@ -8,7 +8,8 @@ import (
"testing"
pmgsandbox "github.com/safedep/pmg/sandbox"
"github.com/safedep/pmg/usefulerror"
"github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/errcodes"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -97,7 +98,7 @@ func TestProfileInit_RefuseOverwrite(t *testing.T) {
assert.Contains(t, err.Error(), target)
usefulErr, ok := usefulerror.AsUsefulError(err)
require.True(t, ok)
assert.Equal(t, usefulerror.ErrCodeInvalidArgument, usefulErr.Code())
assert.Equal(t, errcodes.InvalidArgument, usefulErr.Code())
data, err := os.ReadFile(target)
require.NoError(t, err)
@@ -153,7 +154,7 @@ func TestProfileInit_InvalidName(t *testing.T) {
assert.Contains(t, err.Error(), "invalid profile name")
usefulErr, ok := usefulerror.AsUsefulError(err)
require.True(t, ok)
assert.Equal(t, usefulerror.ErrCodeInvalidArgument, usefulErr.Code())
assert.Equal(t, errcodes.InvalidArgument, usefulErr.Code())
})
}
}
@@ -166,7 +167,7 @@ func TestProfileInit_UnknownBuiltin(t *testing.T) {
assert.Contains(t, err.Error(), "unknown built-in profile")
usefulErr, ok := usefulerror.AsUsefulError(err)
require.True(t, ok)
assert.Equal(t, usefulerror.ErrCodeNotFound, usefulErr.Code())
assert.Equal(t, errcodes.NotFound, usefulErr.Code())
}
func TestProfileInit_StdoutIsExactlyThePath(t *testing.T) {
+4 -3
View File
@@ -11,7 +11,8 @@ import (
"github.com/stretchr/testify/require"
pmgsandbox "github.com/safedep/pmg/sandbox"
"github.com/safedep/pmg/usefulerror"
"github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/errcodes"
)
func writeUserProfileLint(t *testing.T, dir, name, body string) string {
@@ -178,7 +179,7 @@ func TestProfileLint_UnknownProfile(t *testing.T) {
assert.Contains(t, err.Error(), "not found")
usefulErr, ok := usefulerror.AsUsefulError(err)
require.True(t, ok)
assert.Equal(t, usefulerror.ErrCodeNotFound, usefulErr.Code())
assert.Equal(t, errcodes.NotFound, usefulErr.Code())
}
func TestProfileLintMissingTargetShowsUsage(t *testing.T) {
@@ -211,7 +212,7 @@ func TestProfileLint_InvalidYAMLReturnsInvalidArgument(t *testing.T) {
require.Error(t, err)
usefulErr, ok := usefulerror.AsUsefulError(err)
require.True(t, ok)
assert.Equal(t, usefulerror.ErrCodeInvalidArgument, usefulErr.Code())
assert.Equal(t, errcodes.InvalidArgument, usefulErr.Code())
}
func TestProfileLint_LiteralPath(t *testing.T) {
+2 -2
View File
@@ -7,7 +7,7 @@ import (
"github.com/safedep/pmg/internal/ui"
pmgsandbox "github.com/safedep/pmg/sandbox"
"github.com/safedep/pmg/usefulerror"
"github.com/safedep/pmg/errcodes"
"github.com/spf13/cobra"
)
@@ -45,7 +45,7 @@ func runProfileList(out io.Writer, opts *profileListOptions, factory registryFac
summaries, err := registry.ListProfiles()
if err != nil {
return wrapUseful(err, ioErrorCode(err, usefulerror.ErrCodeUnknown),
return wrapUseful(err, ioErrorCode(err, errcodes.Unknown),
"Failed to enumerate sandbox profiles. Check the user profile directory permissions.")
}
+4 -3
View File
@@ -14,7 +14,8 @@ import (
"github.com/stretchr/testify/require"
pmgsandbox "github.com/safedep/pmg/sandbox"
"github.com/safedep/pmg/usefulerror"
"github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/errcodes"
)
func newTestRegistry(t *testing.T, userDir string) registryFactory {
@@ -119,7 +120,7 @@ func TestProfileListRegistryFailureReturnsUseful(t *testing.T) {
require.Error(t, err)
usefulErr, ok := usefulerror.AsUsefulError(err)
require.True(t, ok)
assert.Equal(t, usefulerror.ErrCodeUnknown, usefulErr.Code())
assert.Equal(t, errcodes.Unknown, usefulErr.Code())
assert.Contains(t, err.Error(), "boom")
}
@@ -138,7 +139,7 @@ func TestProfileListRegistryPermissionErrorReturnsPermissionDenied(t *testing.T)
require.Error(t, err)
usefulErr, ok := usefulerror.AsUsefulError(err)
require.True(t, ok)
assert.Equal(t, usefulerror.ErrCodePermissionDenied, usefulErr.Code())
assert.Equal(t, errcodes.PermissionDenied, usefulErr.Code())
}
func TestProfileListRejectsUnexpectedArgs(t *testing.T) {
+3 -3
View File
@@ -8,7 +8,7 @@ import (
"github.com/safedep/dry/log"
pmgsandbox "github.com/safedep/pmg/sandbox"
"github.com/safedep/pmg/sandbox/platform"
"github.com/safedep/pmg/usefulerror"
"github.com/safedep/pmg/errcodes"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
)
@@ -108,7 +108,7 @@ func runProfileShowResolved(out io.Writer, name string, opts *profileShowOptions
data, err := yaml.Marshal(policy)
if err != nil {
return wrapUseful(fmt.Errorf("failed to marshal resolved policy: %w", err),
usefulerror.ErrCodeUnknown,
errcodes.Unknown,
"Failed to marshal the resolved policy to YAML. Re-run with --verbose for the underlying cause.")
}
@@ -127,7 +127,7 @@ func runProfileShowDriver(out io.Writer, name string, opts *profileShowOptions,
rendered, err := platform.Render(pmgsandbox.DriverName(opts.driver), policy)
if err != nil {
return wrapUseful(err, usefulerror.ErrCodeInvalidArgument,
return wrapUseful(err, errcodes.InvalidArgument,
"Could not render the policy for the requested driver. Verify the driver is supported on this host.")
}
+5 -4
View File
@@ -7,7 +7,8 @@ import (
"strings"
"testing"
"github.com/safedep/pmg/usefulerror"
"github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/errcodes"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -71,7 +72,7 @@ func TestProfileShowUnknownDriver(t *testing.T) {
assert.Contains(t, err.Error(), "unknown driver")
usefulErr, ok := usefulerror.AsUsefulError(err)
require.True(t, ok)
assert.Equal(t, usefulerror.ErrCodeInvalidArgument, usefulErr.Code())
assert.Equal(t, errcodes.InvalidArgument, usefulErr.Code())
}
func TestProfileShowUnknownProfileReturnsNotFound(t *testing.T) {
@@ -88,7 +89,7 @@ func TestProfileShowUnknownProfileReturnsNotFound(t *testing.T) {
assert.Contains(t, err.Error(), "not found")
usefulErr, ok := usefulerror.AsUsefulError(err)
require.True(t, ok)
assert.Equal(t, usefulerror.ErrCodeNotFound, usefulErr.Code())
assert.Equal(t, errcodes.NotFound, usefulErr.Code())
}
func TestProfileShowMissingNameShowsUsage(t *testing.T) {
@@ -150,5 +151,5 @@ func TestProfileShowDriverNonNativeErrors(t *testing.T) {
assert.Contains(t, err.Error(), "not available")
usefulErr, ok := usefulerror.AsUsefulError(err)
require.True(t, ok, "non-native driver error should be reported as a UsefulError")
assert.Equal(t, usefulerror.ErrCodeInvalidArgument, usefulErr.Code())
assert.Equal(t, errcodes.InvalidArgument, usefulErr.Code())
}
+5 -4
View File
@@ -8,7 +8,8 @@ import (
"github.com/safedep/pmg/internal/ui"
pmgsandbox "github.com/safedep/pmg/sandbox"
"github.com/safedep/pmg/usefulerror"
"github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/errcodes"
"github.com/spf13/cobra"
)
@@ -27,7 +28,7 @@ func (e *violationsListFailError) ExitCode() int { return ExitCodeViolationsList
func newViolationsListFailError(code, msg, help string) *violationsListFailError {
return &violationsListFailError{
UsefulError: usefulerror.Useful().
UsefulError: usefulerror.NewUsefulError().
WithCode(code).
WithHumanError(msg).
WithHelp(help).
@@ -61,7 +62,7 @@ func newViolationsListCommand(factory cacheFactory) *cobra.Command {
func runViolationsList(out, errOut io.Writer, opts *violationsListOptions, factory cacheFactory) error {
if opts.limit < 0 {
return newViolationsListFailError(
usefulerror.ErrCodeInvalidArgument,
errcodes.InvalidArgument,
fmt.Sprintf("invalid --limit %d (must be >= 0)", opts.limit),
"Pass --limit 0 to show all entries, or a positive limit.",
)
@@ -71,7 +72,7 @@ func runViolationsList(out, errOut io.Writer, opts *violationsListOptions, facto
entries, err := cache.List()
if err != nil {
return newViolationsListFailError(
usefulerror.ErrCodeUnknown,
errcodes.Unknown,
fmt.Sprintf("read cache: %v", err),
"Check the sandbox violation cache directory and retry.",
)
+3 -2
View File
@@ -7,7 +7,8 @@ import (
"testing"
pmgsandbox "github.com/safedep/pmg/sandbox"
"github.com/safedep/pmg/usefulerror"
"github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/errcodes"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -108,7 +109,7 @@ func TestViolationsListRejectsNegativeLimit(t *testing.T) {
assert.Contains(t, err.Error(), "invalid --limit")
usefulErr, ok := usefulerror.AsUsefulError(err)
require.True(t, ok)
assert.Equal(t, usefulerror.ErrCodeInvalidArgument, usefulErr.Code())
assert.Equal(t, errcodes.InvalidArgument, usefulErr.Code())
}
func TestViolationsList_LimitZeroReturnsAll(t *testing.T) {
+4 -3
View File
@@ -13,7 +13,8 @@ import (
packagev1 "buf.build/gen/go/safedep/api/protocolbuffers/go/safedep/messages/package/v1"
"github.com/safedep/dry/log"
"github.com/safedep/dry/utils"
"github.com/safedep/pmg/usefulerror"
"github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/errcodes"
"github.com/spf13/viper"
)
@@ -676,8 +677,8 @@ func NewManagedConfigError() error {
// managedError builds the standard "globally managed" CLI error with a useful
// code and actionable help.
func managedError(message string) error {
return usefulerror.Useful().
WithCode(usefulerror.ErrCodePermissionDenied).
return usefulerror.NewUsefulError().
WithCode(errcodes.PermissionDenied).
WithHumanError(message).
WithHelp("This machine's PMG configuration is centrally managed. Contact your administrator to change it.").
Wrap(errors.New(message))
+16
View File
@@ -0,0 +1,16 @@
package errcodes
const (
InvalidArgument = "InvalidArgument"
PermissionDenied = "PermissionDenied"
NotFound = "NotFound"
Timeout = "Timeout"
Canceled = "Canceled"
UnexpectedEOF = "UnexpectedEOF"
Unknown = "Unknown"
Lifecycle = "Lifecycle"
Network = "Network"
SandboxViolation = "SandboxViolation"
PackageManagerExecutionFailed = "PackageManagerExecutionFailed"
BubblewrapNotFound = "bubblewrap_not_found"
)
+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())
+18 -17
View File
@@ -1,7 +1,8 @@
package packagemanager
import (
"github.com/safedep/pmg/usefulerror"
"github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/errcodes"
)
const (
@@ -12,44 +13,44 @@ const (
)
var (
ErrPackageNotFound = usefulerror.Useful().
WithCode(usefulerror.ErrCodeNotFound).
ErrPackageNotFound = usefulerror.NewUsefulError().
WithCode(errcodes.NotFound).
WithHumanError("The requested package could not be found.").
WithHelp("Please check the package name and try again.")
ErrFailedToFetchPackage = usefulerror.Useful().
WithCode(usefulerror.ErrCodeNetwork).
ErrFailedToFetchPackage = usefulerror.NewUsefulError().
WithCode(errcodes.Network).
WithHumanError("Failed to retrieve the requested package.").
WithHelp("Check your network connection and try again.").
Msg("failed to fetch package")
WithMsg("failed to fetch package")
ErrFailedToResolveVersion = usefulerror.Useful().
WithCode(usefulerror.ErrCodeNetwork).
ErrFailedToResolveVersion = usefulerror.NewUsefulError().
WithCode(errcodes.Network).
WithHumanError("Failed to resolve the requested package version.").
WithHelp("Check your network connection and try again.").
Msg("failed to resolve package version")
WithMsg("failed to resolve package version")
ErrFailedToResolveDependencies = usefulerror.Useful().
ErrFailedToResolveDependencies = usefulerror.NewUsefulError().
WithCode(errDependencyResolutionFailed).
WithHumanError("Failed to resolve dependencies.").
WithHelp("Check your network connection and try again.").
Msg("failed to resolve dependencies")
WithMsg("failed to resolve dependencies")
ErrFailedToParsePackage = usefulerror.Useful().
ErrFailedToParsePackage = usefulerror.NewUsefulError().
WithCode(errPackageParseFailed).
WithHumanError("The package data could not be processed.").
WithHelp("The package may be corrupted or in an unsupported format.").
Msg("failed to parse package")
WithMsg("failed to parse package")
ErrAuthorNotFound = usefulerror.Useful().
ErrAuthorNotFound = usefulerror.NewUsefulError().
WithCode(errPackageAuthorNotFound).
WithHumanError("The package author information could not be found.").
WithHelp("This may be due to incomplete package metadata or network issues.").
Msg("author not found")
WithMsg("author not found")
ErrGitHubRateLimitExceeded = usefulerror.Useful().
ErrGitHubRateLimitExceeded = usefulerror.NewUsefulError().
WithCode(errGitHubRateLimitExceeded).
WithHumanError("GitHub API rate limit has been exceeded.").
WithHelp("Wait for the rate limit to reset or configure authentication to increase your rate limit.").
Msg("github api rate limit exceeded")
WithMsg("github api rate limit exceeded")
)
+8 -7
View File
@@ -12,7 +12,8 @@ import (
"github.com/safedep/pmg/internal/audit"
"github.com/safedep/pmg/sandbox"
"github.com/safedep/pmg/sandbox/platform"
"github.com/safedep/pmg/usefulerror"
"github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/errcodes"
)
type applySandboxConfig struct {
@@ -58,8 +59,8 @@ func ApplySandbox(ctx context.Context, cmd *exec.Cmd, pmName string, opts ...app
policy, err = registry.GetProfile(cfg.SandboxProfileOverride)
if err != nil {
return nil, usefulerror.Useful().
WithCode(usefulerror.ErrCodeInvalidArgument).
return nil, usefulerror.NewUsefulError().
WithCode(errcodes.InvalidArgument).
WithHumanError(fmt.Sprintf("Failed to load sandbox profile override: %s", cfg.SandboxProfileOverride)).
WithHelp("Please verify the sandbox profile path and try again.").
WithAdditionalHelp("See more at: https://github.com/safedep/pmg/blob/main/docs/sandbox.md").
@@ -74,8 +75,8 @@ func ApplySandbox(ctx context.Context, cmd *exec.Cmd, pmName string, opts ...app
// disable for the package manager in the config.
policyRef, exists := cfg.Config.Sandbox.Policies[pmName]
if !exists {
return nil, usefulerror.Useful().
WithCode(usefulerror.ErrCodeNotFound).
return nil, usefulerror.NewUsefulError().
WithCode(errcodes.NotFound).
WithHumanError(fmt.Sprintf("No sandbox policy configured for %s", pmName)).
WithHelp("Please configure a sandbox policy for this package manager in the config file.").
WithAdditionalHelp("See https://github.com/safedep/pmg/blob/main/docs/sandbox.md for more information.").
@@ -137,8 +138,8 @@ func ApplySandbox(ctx context.Context, cmd *exec.Cmd, pmName string, opts ...app
}
if !sb.IsAvailable() {
return nil, usefulerror.Useful().
WithCode(usefulerror.ErrCodeInvalidArgument).
return nil, usefulerror.NewUsefulError().
WithCode(errcodes.InvalidArgument).
WithHumanError(fmt.Sprintf("Sandbox %s is required but not available", sb.Name())).
WithHelp("Please install the sandbox provider and try again.").
WithAdditionalHelp("See more at: https://github.com/safedep/pmg/blob/main/docs/sandbox.md").
+4 -3
View File
@@ -6,7 +6,8 @@ import (
"github.com/safedep/dry/log"
"github.com/safedep/pmg/config"
"github.com/safedep/pmg/sandbox"
"github.com/safedep/pmg/usefulerror"
"github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/errcodes"
)
// WrapCommandExecutionError converts a package manager execution error into a
@@ -28,8 +29,8 @@ func WrapCommandExecutionError(err error, result *sandbox.ExecutionResult, exitC
}
help := "Check the package manager command and its arguments"
builder := usefulerror.Useful().
WithCode(usefulerror.ErrCodePackageManagerExecutionFailed).
builder := usefulerror.NewUsefulError().
WithCode(errcodes.PackageManagerExecutionFailed).
WithHumanError(humanError).
WithHelp(help)
+4 -3
View File
@@ -7,7 +7,8 @@ import (
"testing"
"github.com/safedep/pmg/sandbox"
"github.com/safedep/pmg/usefulerror"
"github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/errcodes"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -61,7 +62,7 @@ func TestWrapCommandExecutionErrorDoesNotAttributeFailureToSandbox(t *testing.T)
usefulErr, ok := usefulerror.AsUsefulError(err)
require.True(t, ok)
assert.Equal(t, usefulerror.ErrCodePackageManagerExecutionFailed, usefulErr.Code())
assert.Equal(t, errcodes.PackageManagerExecutionFailed, usefulErr.Code())
assert.Equal(t, "Package manager command exited with code: 1", usefulErr.HumanError())
assert.NotContains(t, usefulErr.Help(), "./.env")
assert.Contains(t, usefulErr.AdditionalHelp(), "pmg sandbox violations list")
@@ -76,7 +77,7 @@ func TestWrapCommandExecutionErrorOmitsBreadcrumbWhenNoViolations(t *testing.T)
usefulErr, ok := usefulerror.AsUsefulError(err)
require.True(t, ok)
assert.Equal(t, usefulerror.ErrCodePackageManagerExecutionFailed, usefulErr.Code())
assert.Equal(t, errcodes.PackageManagerExecutionFailed, usefulErr.Code())
assert.NotContains(t, usefulErr.AdditionalHelp(), "pmg sandbox violations list")
}
+4 -3
View File
@@ -9,8 +9,9 @@ import (
"os/exec"
"github.com/safedep/dry/log"
"github.com/safedep/dry/usefulerror"
"github.com/safedep/pmg/errcodes"
"github.com/safedep/pmg/sandbox"
"github.com/safedep/pmg/usefulerror"
)
// bubblewrapSandbox implements the Sandbox interface using Bubblewrap (bwrap) on Linux.
@@ -45,8 +46,8 @@ func newBubblewrapSandbox() (*bubblewrapSandbox, error) {
func (b *bubblewrapSandbox) Execute(ctx context.Context, cmd *exec.Cmd, policy *sandbox.SandboxPolicy) (*sandbox.ExecutionResult, error) {
bwrapPath, err := exec.LookPath("bwrap")
if err != nil {
return nil, usefulerror.Useful().
WithCode("bubblewrap_not_found").
return nil, usefulerror.NewUsefulError().
WithCode(errcodes.BubblewrapNotFound).
WithHumanError("Bubblewrap binary not found").
WithHelp("See more at: https://github.com/safedep/pmg/blob/main/docs/sandbox.md").
Wrap(fmt.Errorf("bubblewrap binary not found: %w", err))
-18
View File
@@ -1,18 +0,0 @@
package usefulerror
// Standard error codes that can be re-used across the project.
// We will use a human friendly format for the error codes and not align with posix error codes.
// Keep this minimal. Reuse first before adding new ones.
const (
ErrCodeInvalidArgument = "InvalidArgument"
ErrCodePermissionDenied = "PermissionDenied"
ErrCodeNotFound = "NotFound"
ErrCodeTimeout = "Timeout"
ErrCodeCanceled = "Canceled"
ErrCodeUnexpectedEOF = "UnexpectedEOF"
ErrCodeUnknown = "Unknown"
ErrCodeLifecycle = "Lifecycle"
ErrCodeNetwork = "Network"
ErrCodeSandboxViolation = "SandboxViolation"
ErrCodePackageManagerExecutionFailed = "PackageManagerExecutionFailed"
)
-158
View File
@@ -1,158 +0,0 @@
package usefulerror
import (
"errors"
"strings"
)
// UsefulError is an interface that can be implemented for custom error types
// that are actually useful for the user. Think of this as a way out of showing
// weird internal errors to the user, which actually don't help them
type UsefulError interface {
// Error returns a string that is useful for the user.
// Maintains compatibility with the standard error interface.
Error() string
// HumanError returns a string that is more human-readable.
HumanError() string
// Help returns a string that provides help or guidance specific to the
// business logic of the error.
Help() string
// AdditionalHelp returns a string that provides additional help or guidance
// This is useful for providing specific tooling related instructions such
// as common line flags to use to fix the error.
AdditionalHelp() string
// Code returns a string that can be used to identify the error types
// Meant for programmatic use, such as logging or categorization
Code() string
}
type usefulErrorBuilder struct {
originalError error
humanError string
help string
additionalHelp string
code string
msg string
}
var _ UsefulError = (*usefulErrorBuilder)(nil)
func Useful() *usefulErrorBuilder {
return &usefulErrorBuilder{}
}
func (b *usefulErrorBuilder) Wrap(originalError error) *usefulErrorBuilder {
b.originalError = originalError
return b
}
// WithHumanError sets a string that is more human-readable.
func (b *usefulErrorBuilder) WithHumanError(humanError string) *usefulErrorBuilder {
b.humanError = humanError
return b
}
// WithHelp sets a string that provides additional help or guidance.
func (b *usefulErrorBuilder) WithHelp(help string) *usefulErrorBuilder {
b.help = help
return b
}
// WithCode sets a code that can be used to identify the error types.
func (b *usefulErrorBuilder) WithCode(code string) *usefulErrorBuilder {
b.code = code
return b
}
// WithMsg sets a message that is useful for the user, but not necessarily human-readable.
func (b *usefulErrorBuilder) Msg(msg string) *usefulErrorBuilder {
b.msg = msg
return b
}
// WithAdditionalHelp sets a string that provides additional help or guidance.
func (b *usefulErrorBuilder) WithAdditionalHelp(additionalHelp string) *usefulErrorBuilder {
b.additionalHelp = additionalHelp
return b
}
// Error implements the standard error interface. It returns the original error's message if present;
// otherwise, it returns a constructed message based on the code and msg fields, or "unknown error" if none are set.
func (b *usefulErrorBuilder) Error() string {
if b.originalError != nil {
return b.originalError.Error()
}
if b.msg == "" {
return "unknown error"
}
msgParts := []string{}
if b.code != "" {
msgParts = append(msgParts, b.code)
}
if b.msg != "" {
msgParts = append(msgParts, b.msg)
}
return strings.Join(msgParts, ": ")
}
// HumanError returns a string that is more human-readable.
func (b *usefulErrorBuilder) HumanError() string {
if b.humanError == "" {
return "An error occurred, but no human-readable message is available."
}
return b.humanError
}
// Help returns a string that provides additional help or guidance.
func (b *usefulErrorBuilder) Help() string {
if b.help == "" {
return "No additional help is available for this error."
}
return b.help
}
// Code returns a string that can be used to identify the error types.
func (b *usefulErrorBuilder) Code() string {
if b.code == "" {
return "unknown"
}
return b.code
}
// AdditionalHelp returns a string that provides additional help or guidance.
func (b *usefulErrorBuilder) AdditionalHelp() string {
if b.additionalHelp == "" {
return "No additional help is available for this error."
}
return b.additionalHelp
}
// AsUsefulError attempts to convert a given error into a UsefulError.
func AsUsefulError(err error) (UsefulError, bool) {
if err == nil {
return nil, false
}
var usefulErr *usefulErrorBuilder
if errors.As(err, &usefulErr) {
return usefulErr, true
}
if usefulErr, ok := err.(UsefulError); ok {
return usefulErr, true
}
return nil, false
}
-271
View File
@@ -1,271 +0,0 @@
package usefulerror
import (
"errors"
"testing"
"github.com/stretchr/testify/assert"
)
func TestUsefulErrorBuilder_Error(t *testing.T) {
tests := []struct {
name string
builder func() *usefulErrorBuilder
expected string
}{
{
name: "with original error",
builder: func() *usefulErrorBuilder {
return Useful().Wrap(errors.New("original error"))
},
expected: "original error",
},
{
name: "with msg only",
builder: func() *usefulErrorBuilder {
return Useful().Msg("test message")
},
expected: "test message",
},
{
name: "with code and msg",
builder: func() *usefulErrorBuilder {
return Useful().WithCode("TEST001").Msg("test message")
},
expected: "TEST001: test message",
},
{
name: "with code only",
builder: func() *usefulErrorBuilder {
return Useful().WithCode("TEST001")
},
expected: "unknown error",
},
{
name: "empty builder",
builder: func() *usefulErrorBuilder {
return Useful()
},
expected: "unknown error",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.builder()
assert.Equal(t, tt.expected, err.Error())
})
}
}
func TestUsefulErrorBuilder_HumanError(t *testing.T) {
tests := []struct {
name string
builder func() *usefulErrorBuilder
expected string
}{
{
name: "with human error set",
builder: func() *usefulErrorBuilder {
return Useful().WithHumanError("Something went wrong")
},
expected: "Something went wrong",
},
{
name: "empty human error",
builder: func() *usefulErrorBuilder {
return Useful()
},
expected: "An error occurred, but no human-readable message is available.",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.builder()
assert.Equal(t, tt.expected, err.HumanError())
})
}
}
func TestUsefulErrorBuilder_Help(t *testing.T) {
tests := []struct {
name string
builder func() *usefulErrorBuilder
expected string
}{
{
name: "with help set",
builder: func() *usefulErrorBuilder {
return Useful().WithHelp("Try running with --verbose flag")
},
expected: "Try running with --verbose flag",
},
{
name: "empty help",
builder: func() *usefulErrorBuilder {
return Useful()
},
expected: "No additional help is available for this error.",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.builder()
assert.Equal(t, tt.expected, err.Help())
})
}
}
func TestUsefulErrorBuilder_AdditionalHelp(t *testing.T) {
tests := []struct {
name string
builder func() *usefulErrorBuilder
expected string
}{
{
name: "with additional help set",
builder: func() *usefulErrorBuilder {
return Useful().WithAdditionalHelp("Use --force to override")
},
expected: "Use --force to override",
},
{
name: "empty additional help",
builder: func() *usefulErrorBuilder {
return Useful()
},
expected: "No additional help is available for this error.",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.builder()
assert.Equal(t, tt.expected, err.AdditionalHelp())
})
}
}
func TestUsefulErrorBuilder_Code(t *testing.T) {
tests := []struct {
name string
builder func() *usefulErrorBuilder
expected string
}{
{
name: "with code set",
builder: func() *usefulErrorBuilder {
return Useful().WithCode("ERR001")
},
expected: "ERR001",
},
{
name: "empty code",
builder: func() *usefulErrorBuilder {
return Useful()
},
expected: "unknown",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.builder()
assert.Equal(t, tt.expected, err.Code())
})
}
}
func TestUsefulErrorBuilder_ChainedMethods(t *testing.T) {
err := Useful().
WithCode("TEST001").
Msg("test message").
WithHumanError("User friendly error").
WithHelp("Try this fix").
WithAdditionalHelp("Or try this")
assert.Equal(t, "TEST001: test message", err.Error())
assert.Equal(t, "User friendly error", err.HumanError())
assert.Equal(t, "Try this fix", err.Help())
assert.Equal(t, "Or try this", err.AdditionalHelp())
assert.Equal(t, "TEST001", err.Code())
}
func TestAsUsefulError(t *testing.T) {
tests := []struct {
name string
input error
expectOk bool
expectError bool
}{
{
name: "nil error",
input: nil,
expectOk: false,
expectError: false,
},
{
name: "useful error builder",
input: Useful().Msg("test"),
expectOk: true,
expectError: false,
},
{
name: "regular error",
input: errors.New("regular error"),
expectOk: false,
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, ok := AsUsefulError(tt.input)
assert.Equal(t, tt.expectOk, ok)
if tt.expectOk {
assert.NotNil(t, result)
assert.Implements(t, (*UsefulError)(nil), result)
} else {
assert.Nil(t, result)
}
})
}
}
func TestUsefulErrorBuilder_ImplementsUsefulError(t *testing.T) {
var _ UsefulError = (*usefulErrorBuilder)(nil)
builder := Useful()
assert.Implements(t, (*UsefulError)(nil), builder)
}
func TestUsefulErrorBuilder_Wrap(t *testing.T) {
originalErr := errors.New("original error")
wrappedErr := Useful().Wrap(originalErr)
assert.Equal(t, "original error", wrappedErr.Error())
assert.Equal(t, "An error occurred, but no human-readable message is available.", wrappedErr.HumanError())
}
func TestUsefulErrorBuilder_ComplexScenario(t *testing.T) {
originalErr := errors.New("file not found")
err := Useful().
Wrap(originalErr).
WithCode("FILE001").
WithHumanError("The configuration file could not be found").
WithHelp("Make sure the config file exists in the current directory").
WithAdditionalHelp("Use --config flag to specify a different location")
assert.Equal(t, "file not found", err.Error())
assert.Equal(t, "The configuration file could not be found", err.HumanError())
assert.Equal(t, "Make sure the config file exists in the current directory", err.Help())
assert.Equal(t, "Use --config flag to specify a different location", err.AdditionalHelp())
assert.Equal(t, "FILE001", err.Code())
usefulErr, ok := AsUsefulError(err)
assert.True(t, ok)
assert.Equal(t, err, usefulErr)
}