From b489a3bba15bc6d808d7cc9c17f74dd833d32b9e Mon Sep 17 00:00:00 2001 From: Ophestra Date: Sun, 31 Aug 2025 13:51:21 +0900 Subject: [PATCH] system/output: implement MessageError This error is also formatted differently based on state. Signed-off-by: Ophestra --- system/output.go | 34 +++++++++++++++++++------- system/output_test.go | 56 ++++++++++++++++++++++++++++++++++++++----- 2 files changed, 76 insertions(+), 14 deletions(-) diff --git a/system/output.go b/system/output.go index f3a4bc5..e602976 100644 --- a/system/output.go +++ b/system/output.go @@ -20,16 +20,16 @@ func SetOutput(v container.Msg) { // OpError is returned by [I.Commit] and [I.Revert]. type OpError struct { - Op string - Err error - Message string - Revert bool + Op string + Err error + Msg string + Revert bool } func (e *OpError) Unwrap() error { return e.Err } func (e *OpError) Error() string { - if e.Message != "" { - return e.Message + if e.Msg != "" { + return e.Msg } switch { @@ -47,6 +47,16 @@ func (e *OpError) Error() string { } } +func (e *OpError) Message() string { + switch { + case e.Msg != "": + return e.Error() + + default: + return "cannot " + e.Error() + } +} + // newOpError returns an [OpError] without a message string. func newOpError(op string, err error, revert bool) error { if err == nil { @@ -69,10 +79,18 @@ func printJoinedError(println func(v ...any), fallback string, err error) { error } if !errors.As(err, &joinErr) { - println(fallback, err) + if m, ok := container.GetErrorMessage(err); ok { + println(m) + } else { + println(fallback, err) + } } else { for _, err = range joinErr.Unwrap() { - println(err.Error()) + if m, ok := container.GetErrorMessage(err); ok { + println(m) + } else { + println(err.Error()) + } } } } diff --git a/system/output_test.go b/system/output_test.go index ba760e8..9ad949c 100644 --- a/system/output_test.go +++ b/system/output_test.go @@ -19,27 +19,33 @@ func TestOpError(t *testing.T) { s string is error isF error + msg string }{ {"message", newOpErrorMessage("dbus", ErrDBusConfig, "attempted to create message bus proxy args without session bus config", false), "attempted to create message bus proxy args without session bus config", - ErrDBusConfig, syscall.ENOTRECOVERABLE}, + ErrDBusConfig, syscall.ENOTRECOVERABLE, + "attempted to create message bus proxy args without session bus config"}, {"apply", newOpError("tmpfile", syscall.EBADE, false), "apply tmpfile: invalid exchange", - syscall.EBADE, syscall.EBADF}, + syscall.EBADE, syscall.EBADF, + "cannot apply tmpfile: invalid exchange"}, {"revert", newOpError("wayland", syscall.EBADF, true), "revert wayland: bad file descriptor", - syscall.EBADF, syscall.EBADE}, + syscall.EBADF, syscall.EBADE, + "cannot revert wayland: bad file descriptor"}, {"path", newOpError("tmpfile", &os.PathError{Op: "stat", Path: "/run/dbus", Err: syscall.EISDIR}, false), "stat /run/dbus: is a directory", - syscall.EISDIR, syscall.ENOTDIR}, + syscall.EISDIR, syscall.ENOTDIR, + "cannot stat /run/dbus: is a directory"}, {"net", newOpError("wayland", &net.OpError{Op: "dial", Net: "unix", Addr: &net.UnixAddr{Name: "/run/user/1000/wayland-1", Net: "unix"}, Err: syscall.ENOENT}, false), "dial unix /run/user/1000/wayland-1: no such file or directory", - syscall.ENOENT, syscall.EPERM}, + syscall.ENOENT, syscall.EPERM, + "cannot dial unix /run/user/1000/wayland-1: no such file or directory"}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { @@ -48,6 +54,7 @@ func TestOpError(t *testing.T) { t.Errorf("Error: %q, want %q", got, tc.s) } }) + t.Run("is", func(t *testing.T) { if !errors.Is(tc.err, tc.is) { t.Error("Is: unexpected false") @@ -56,6 +63,17 @@ func TestOpError(t *testing.T) { t.Error("Is: unexpected true") } }) + + t.Run("msg", func(t *testing.T) { + if got, ok := container.GetErrorMessage(tc.err); !ok { + if tc.msg != "" { + t.Errorf("GetErrorMessage: err does not implement MessageError") + } + return + } else if got != tc.msg { + t.Errorf("GetErrorMessage: %q, want %q", got, tc.msg) + } + }) }) } @@ -103,14 +121,40 @@ func TestPrintJoinedError(t *testing.T) { want [][]any }{ {"nil", nil, [][]any{{"not a joined error:", nil}}}, - {"unwrapped", syscall.EINVAL, [][]any{{"not a joined error:", syscall.EINVAL}}}, {"single", errors.Join(syscall.EINVAL), [][]any{{"invalid argument"}}}, + {"unwrapped", syscall.EINVAL, [][]any{{"not a joined error:", syscall.EINVAL}}}, + {"unwrapped message", &OpError{ + Op: "meow", + Err: syscall.EBADFD, + }, [][]any{ + {"cannot apply meow: file descriptor in bad state"}, + }}, + {"many", errors.Join(syscall.ENOTRECOVERABLE, syscall.ETIMEDOUT, syscall.EBADFD), [][]any{ {"state not recoverable"}, {"connection timed out"}, {"file descriptor in bad state"}, }}, + {"many message", errors.Join( + &container.StartError{ + Step: "meow", + Err: syscall.ENOMEM, + }, + &os.PathError{ + Op: "meow", + Path: "/proc/nonexistent", + Err: syscall.ENOSYS, + }, + &OpError{ + Op: "meow", + Err: syscall.ENODEV, + Revert: true, + }), [][]any{ + {"cannot meow: cannot allocate memory"}, + {"meow /proc/nonexistent: function not implemented"}, + {"cannot revert meow: no such device"}, + }}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) {