From 8690419c2d577a27efe3cfcafc39c05b7646926b Mon Sep 17 00:00:00 2001 From: Ophestra Date: Mon, 15 Sep 2025 01:44:43 +0900 Subject: hst: replace internal/app error This turns out to still be quite useful across internal/app and its relatives. Perhaps a cleaner replacement for baseError. Signed-off-by: Ophestra --- hst/hst.go | 30 +++++++++++++++++++++ hst/hst_test.go | 82 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 112 insertions(+) (limited to 'hst') diff --git a/hst/hst.go b/hst/hst.go index d9c1168b..c9a23d7a 100644 --- a/hst/hst.go +++ b/hst/hst.go @@ -2,12 +2,42 @@ package hst import ( + "errors" + "net" + "os" + "hakurei.app/container" "hakurei.app/container/seccomp" "hakurei.app/system" "hakurei.app/system/dbus" ) +// An AppError is returned while starting an app according to [hst.Config]. +type AppError struct { + Step string + Err error + Msg string +} + +func (e *AppError) Error() string { return e.Err.Error() } +func (e *AppError) Unwrap() error { return e.Err } +func (e *AppError) Message() string { + if e.Msg != "" { + return e.Msg + } + + switch { + case errors.As(e.Err, new(*os.PathError)), + errors.As(e.Err, new(*os.LinkError)), + errors.As(e.Err, new(*os.SyscallError)), + errors.As(e.Err, new(*net.OpError)): + return "cannot " + e.Error() + + default: + return "cannot " + e.Step + ": " + e.Error() + } +} + // Paths contains environment-dependent paths used by hakurei. type Paths struct { // temporary directory returned by [os.TempDir] (usually `/tmp`) diff --git a/hst/hst_test.go b/hst/hst_test.go index 04057212..70f7e531 100644 --- a/hst/hst_test.go +++ b/hst/hst_test.go @@ -2,11 +2,93 @@ package hst_test import ( "encoding/json" + "errors" + "net" + "os" + "syscall" "testing" + "hakurei.app/container" + "hakurei.app/container/stub" "hakurei.app/hst" ) +func TestAppError(t *testing.T) { + testCases := []struct { + name string + err error + s string + message string + is, isF error + }{ + {"message", &hst.AppError{Step: "obtain uid from hsu", Err: stub.UniqueError(0), + Msg: "the setuid helper is missing: /run/wrappers/bin/hsu"}, + "unique error 0 injected by the test suite", + "the setuid helper is missing: /run/wrappers/bin/hsu", + stub.UniqueError(0), os.ErrNotExist}, + + {"os.PathError", &hst.AppError{Step: "passthrough os.PathError", + Err: &os.PathError{Op: "stat", Path: "/proc/nonexistent", Err: os.ErrNotExist}}, + "stat /proc/nonexistent: file does not exist", + "cannot stat /proc/nonexistent: file does not exist", + os.ErrNotExist, stub.UniqueError(0xdeadbeef)}, + + {"os.LinkError", &hst.AppError{Step: "passthrough os.LinkError", + Err: &os.LinkError{Op: "link", Old: "/proc/self", New: "/proc/nonexistent", Err: os.ErrNotExist}}, + "link /proc/self /proc/nonexistent: file does not exist", + "cannot link /proc/self /proc/nonexistent: file does not exist", + os.ErrNotExist, stub.UniqueError(0xdeadbeef)}, + + {"os.SyscallError", &hst.AppError{Step: "passthrough os.SyscallError", + Err: &os.SyscallError{Syscall: "meow", Err: syscall.ENOSYS}}, + "meow: function not implemented", + "cannot meow: function not implemented", + syscall.ENOSYS, syscall.ENOTRECOVERABLE}, + + {"net.OpError", &hst.AppError{Step: "passthrough net.OpError", + Err: &net.OpError{Op: "dial", Net: "cat", Err: net.UnknownNetworkError("cat")}}, + "dial cat: unknown network cat", + "cannot dial cat: unknown network cat", + net.UnknownNetworkError("cat"), syscall.ENOTRECOVERABLE}, + + {"default", &hst.AppError{Step: "initialise container configuration", Err: stub.UniqueError(1)}, + "unique error 1 injected by the test suite", + "cannot initialise container configuration: unique error 1 injected by the test suite", + stub.UniqueError(1), os.ErrInvalid}, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Run("error", func(t *testing.T) { + if got := tc.err.Error(); got != tc.s { + t.Errorf("Error: %s, want %s", got, tc.s) + } + }) + + t.Run("message", func(t *testing.T) { + gotMessage, gotMessageOk := container.GetErrorMessage(tc.err) + if want := tc.message != "\x00"; gotMessageOk != want { + t.Errorf("GetErrorMessage: ok = %v, want %v", gotMessage, want) + } + + if gotMessageOk { + if gotMessage != tc.message { + t.Errorf("GetErrorMessage: %s, want %s", gotMessage, tc.message) + } + } + }) + + t.Run("is", func(t *testing.T) { + if !errors.Is(tc.err, tc.is) { + t.Errorf("Is: unexpected false for %v", tc.is) + } + if errors.Is(tc.err, tc.isF) { + t.Errorf("Is: unexpected true for %v", tc.isF) + } + }) + }) + } +} + func TestTemplate(t *testing.T) { const want = `{ "id": "org.chromium.Chromium", -- cgit v1.3.1