diff options
| author | Ophestra <cat@gensokyo.uk> | 2026-05-26 13:36:28 +0900 |
|---|---|---|
| committer | Ophestra <cat@gensokyo.uk> | 2026-05-26 14:56:16 +0900 |
| commit | 12b9f51128b2683303bc9ea500c07d6f4417988f (patch) | |
| tree | 001bc9f2791ba6776c4cc1337553007c6b87abd5 /internal/report/report_test.go | |
| parent | d15f965d0c5587e79d8da9a514f94be32d817f67 (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.go | 197 |
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)) +} |
