mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
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:
co-authored by
Abhisek Datta
parent
e5fe0df82e
commit
20e01d5cae
+22
-21
@@ -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
@@ -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
@@ -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"))
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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+".")
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.")
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.")
|
||||
}
|
||||
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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.",
|
||||
)
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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.")
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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")
|
||||
)
|
||||
|
||||
@@ -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").
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user