aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/report/report_test.go
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2026-05-26 13:36:28 +0900
committerOphestra <cat@gensokyo.uk>2026-05-26 14:56:16 +0900
commit12b9f51128b2683303bc9ea500c07d6f4417988f (patch)
tree001bc9f2791ba6776c4cc1337553007c6b87abd5 /internal/report/report_test.go
parentd15f965d0c5587e79d8da9a514f94be32d817f67 (diff)
internal/report: report errors with persistent backing
This is useful for reporting errors from processes that must never terminate. Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'internal/report/report_test.go')
-rw-r--r--internal/report/report_test.go197
1 files changed, 197 insertions, 0 deletions
diff --git a/internal/report/report_test.go b/internal/report/report_test.go
new file mode 100644
index 00000000..307968b4
--- /dev/null
+++ b/internal/report/report_test.go
@@ -0,0 +1,197 @@
+package report_test
+
+import (
+ "bytes"
+ "errors"
+ "log"
+ "os"
+ "reflect"
+ "slices"
+ "testing"
+ "unsafe"
+
+ "hakurei.app/check"
+ "hakurei.app/internal/report"
+ "hakurei.app/internal/stub"
+)
+
+type representableUE struct{ stub.UniqueError }
+
+func (representableUE) Representable() {}
+
+func TestDispatch(t *testing.T) {
+ t.Parallel()
+
+ testCases := []struct {
+ name string
+ flag uint64
+ errs []report.Error
+
+ persist bool
+ wantFiles []string
+ wantLog string
+ wantPanic error
+ }{
+ {"default", 0, []report.Error{
+ {
+ Severity: report.Fatal,
+ Message: "rejecting coldboot loop",
+ Err: stub.UniqueError(0xcafe),
+ },
+ }, true, []string{
+ `{"kind":"fatal","message":"rejecting coldboot loop","error":"unique error 51966 injected by the test suite"}
+`,
+ }, "dispatch: rejecting coldboot loop: unique error 51966 injected by the test suite\n", nil},
+
+ {"strict", report.DStrict, []report.Error{
+ {
+ Severity: report.Degraded,
+ Message: "rejecting coldboot loop",
+ Err: stub.UniqueError(0xcafe),
+ },
+ }, true, []string{
+ `{"kind":"degradation","message":"rejecting coldboot loop","error":"unique error 51966 injected by the test suite"}
+`,
+ }, "dispatch: rejecting coldboot loop: unique error 51966 injected by the test suite\n", report.Error{
+ Severity: report.Degraded,
+ Message: "rejecting coldboot loop",
+ Err: stub.UniqueError(0xcafe),
+ }},
+
+ {"strict no recover", report.DStrict | report.DNoRecover, []report.Error{
+ {
+ Severity: report.Inconsistent,
+ Message: "rejecting coldboot loop",
+ Err: stub.UniqueError(0xcafe),
+ },
+ }, true, []string{
+ `{"kind":"inconsistent","message":"rejecting coldboot loop","error":"unique error 51966 injected by the test suite"}
+`,
+ }, "dispatch: rejecting coldboot loop: unique error 51966 injected by the test suite\n", report.Error{
+ Severity: report.Inconsistent,
+ Message: "rejecting coldboot loop",
+ Err: stub.UniqueError(0xcafe),
+ }},
+
+ {"no recover", report.DNoRecover, []report.Error{
+ {
+ Severity: 0xbeef,
+ Message: "rejecting coldboot loop",
+ Err: representableUE{stub.UniqueError(0xcafe)},
+ },
+ }, true, []string{
+ `{"kind":48879,"message":"rejecting coldboot loop","error":{"UniqueError":51966}}
+`,
+ }, "dispatch: rejecting coldboot loop: unique error 51966 injected by the test suite\n", nil},
+
+ {"early", 0, []report.Error{
+ {
+ Severity: report.Fatal,
+ Message: "rejecting coldboot loop",
+ Err: stub.UniqueError(0xcafe),
+ },
+ }, false, nil, "dispatch: rejecting coldboot loop: unique error 51966 injected by the test suite\n", report.Error{
+ Severity: report.Fatal,
+ Message: "rejecting coldboot loop",
+ Err: stub.UniqueError(0xcafe),
+ }},
+
+ {"bypass early", report.DBypassEarly, []report.Error{
+ {
+ Severity: report.Fatal,
+ Message: "rejecting coldboot loop",
+ Err: stub.UniqueError(0xcafe),
+ },
+ }, false, nil, "dispatch: rejecting coldboot loop: unique error 51966 injected by the test suite\n", nil},
+ }
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ t.Parallel()
+
+ var r report.Reporter
+ r.SetFlags(tc.flag)
+ var buf bytes.Buffer
+ l := log.New(&buf, "dispatch: ", 0)
+ r.SetOutput(l)
+
+ a := check.MustAbs(t.TempDir())
+ if tc.persist {
+ r.SetPathname(a)
+ }
+
+ var got []report.Error
+ r.Notify(func(p report.Error) { got = append(got, p) })
+
+ var gotPanic any
+ func() {
+ defer func() { gotPanic = recover() }()
+ for _, p := range tc.errs {
+ r.Dispatch(p.Severity, p.Message, p.Err)
+ }
+ }()
+
+ if gotPanic == nil && !reflect.DeepEqual(got, tc.errs) {
+ t.Errorf("Dispatch: %#v, want %#v", got, tc.errs)
+ }
+ if !reflect.DeepEqual(gotPanic, tc.wantPanic) {
+ t.Errorf("Dispatch: panic = %v, want %v", gotPanic, tc.wantPanic)
+ }
+ if gotLog := buf.String(); gotLog != tc.wantLog {
+ t.Errorf("Dispatch: log =\n%s\nwant\n%s", gotLog, tc.wantLog)
+ }
+
+ if tc.persist {
+ var names []string
+ if dents, err := os.ReadDir(a.String()); err != nil {
+ t.Fatal(err)
+ } else {
+ names = make([]string, len(dents))
+ for i, dent := range dents {
+ names[i] = dent.Name()
+ }
+ slices.Sort(names)
+ }
+
+ gotFiles := make([]string, len(names))
+ for i, name := range names {
+ if p, err := os.ReadFile(a.Append(name).String()); err != nil {
+ t.Fatal(err)
+ } else {
+ gotFiles[i] = unsafe.String(unsafe.SliceData(p), len(p))
+ }
+ }
+
+ if !slices.Equal(gotFiles, tc.wantFiles) {
+ t.Errorf("Dispatch: persist = %s, want %s", gotFiles, tc.wantFiles)
+ }
+ }
+ })
+ }
+}
+
+func TestBadPersist(t *testing.T) {
+ t.Parallel()
+
+ var r report.Reporter
+ r.SetFlags(report.DNoRecover)
+ r.SetPathname(check.MustAbs("/proc/nonexistent"))
+
+ var pathError *os.PathError
+ func() {
+ defer func() {
+ if !errors.As(recover().(error), &pathError) {
+ t.Fatal("invalid panic kind")
+ }
+ }()
+ r.Dispatch(report.Fatal, "\x00", stub.UniqueError(0xbad))
+ }()
+ if !errors.Is(pathError.Err, os.ErrNotExist) {
+ t.Fatalf("Dispatch: panic = %v", pathError)
+ }
+
+ var buf bytes.Buffer
+ l := log.New(&buf, "persist: ", 0)
+ r.SetOutput(l)
+ r.SetFlags(0)
+ r.Dispatch(report.Fatal, "\x00", stub.UniqueError(0xbad))
+}