Strip fx call-chain noise from startup errors; clarify file:// error
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -68,6 +69,24 @@ func NewApp(opts AppOptions) *fx.App {
|
|||||||
return fx.New(allOptions...)
|
return fx.New(allOptions...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// cleanStartupError strips fx's dependency-injection call-chain noise from
|
||||||
|
// startup errors. fx wraps the underlying error with messages like
|
||||||
|
//
|
||||||
|
// could not build arguments for function "X" (file:line): failed to build T:
|
||||||
|
// could not build arguments for function "Y" (file:line): failed to build U:
|
||||||
|
// received non-nil error from function "Z" (file:line): <real error>
|
||||||
|
//
|
||||||
|
// Users care about the real error, not the DI plumbing. We strip everything
|
||||||
|
// up through the last "): " (which is always the close-paren of an fx
|
||||||
|
// function-location annotation followed by the wrapped error).
|
||||||
|
func cleanStartupError(err error) error {
|
||||||
|
msg := err.Error()
|
||||||
|
if idx := strings.LastIndex(msg, "): "); idx >= 0 {
|
||||||
|
msg = msg[idx+3:]
|
||||||
|
}
|
||||||
|
return errors.New(msg)
|
||||||
|
}
|
||||||
|
|
||||||
// RunApp starts and stops the fx application within the given context.
|
// RunApp starts and stops the fx application within the given context.
|
||||||
// It handles graceful shutdown on interrupt signals (SIGINT, SIGTERM) and
|
// It handles graceful shutdown on interrupt signals (SIGINT, SIGTERM) and
|
||||||
// ensures the application stops cleanly. The function blocks until the
|
// ensures the application stops cleanly. The function blocks until the
|
||||||
@@ -83,7 +102,7 @@ func RunApp(ctx context.Context, app *fx.App) error {
|
|||||||
|
|
||||||
// Start the app
|
// Start the app
|
||||||
if err := app.Start(ctx); err != nil {
|
if err := app.Start(ctx); err != nil {
|
||||||
return fmt.Errorf("failed to start app: %w", err)
|
return cleanStartupError(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle shutdown
|
// Handle shutdown
|
||||||
|
|||||||
39
internal/cli/app_test.go
Normal file
39
internal/cli/app_test.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCleanStartupError(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
in string
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "real fx error chain",
|
||||||
|
in: `could not build arguments for function "sneak.berlin/go/vaultik/internal/cli".newSnapshotCreateCommand.func1.1 (/Users/user/dev/vaultik/internal/cli/snapshot.go:71): failed to build *vaultik.Vaultik: could not build arguments for function "sneak.berlin/go/vaultik/internal/vaultik".New (/Users/user/dev/vaultik/internal/vaultik/vaultik.go:59): failed to build storage.Storer: received non-nil error from function "sneak.berlin/go/vaultik/internal/storage".NewStorer (/Users/user/dev/vaultik/internal/storage/module.go:23): creating base path: mkdir /Volumes/BACKUPS: permission denied`,
|
||||||
|
want: `creating base path: mkdir /Volumes/BACKUPS: permission denied`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "no fx wrapping",
|
||||||
|
in: "plain error",
|
||||||
|
want: "plain error",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single fx wrapping",
|
||||||
|
in: `received non-nil error from function "foo" (file.go:1): underlying problem`,
|
||||||
|
want: "underlying problem",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := cleanStartupError(errors.New(tt.in)).Error()
|
||||||
|
if got != tt.want {
|
||||||
|
t.Errorf("got %q, want %q", got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,9 +23,8 @@ type FileStorer struct {
|
|||||||
// Uses the real OS filesystem by default; call SetFilesystem to override for testing.
|
// Uses the real OS filesystem by default; call SetFilesystem to override for testing.
|
||||||
func NewFileStorer(basePath string) (*FileStorer, error) {
|
func NewFileStorer(basePath string) (*FileStorer, error) {
|
||||||
fs := afero.NewOsFs()
|
fs := afero.NewOsFs()
|
||||||
// Ensure base path exists
|
|
||||||
if err := fs.MkdirAll(basePath, 0755); err != nil {
|
if err := fs.MkdirAll(basePath, 0755); err != nil {
|
||||||
return nil, fmt.Errorf("creating base path: %w", err)
|
return nil, fmt.Errorf("file:// storage: cannot create or access %s: %w (check that the volume is mounted and writable)", basePath, err)
|
||||||
}
|
}
|
||||||
return &FileStorer{
|
return &FileStorer{
|
||||||
fs: fs,
|
fs: fs,
|
||||||
|
|||||||
Reference in New Issue
Block a user