package report_test import ( "bytes" "errors" "log" "os" "reflect" "slices" "testing" "unsafe" "hakurei.app/check" "hakurei.app/internal/kobject" "hakurei.app/internal/report" "hakurei.app/internal/stub" "hakurei.app/internal/uevent" ) 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), }, { Severity: report.Inconsistent, Message: "processed inconsistent uevent", Err: (&kobject.Event{ Action: uevent.KOBJ_ADD, DevPath: "\x00", Env: map[string]string{"V": "\xfd"}, Sequence: 2, }).NewError(kobject.EDuplicateAdd, &kobject.Object{ State: kobject.StateNew, DevPath: "\x00", Env: map[string]string{"V": "\xff"}, }), }, }, true, []string{`{"kind":"fatal","message":"rejecting coldboot loop","error":"unique error 51966 injected by the test suite"} `, `{"kind":"inconsistent","message":"processed inconsistent uevent","error":{"fault":1,"event":{"action":"add","devpath":"\u0000","env":{"V":"\ufffd"},"seqnum":2,"subsystem":""},"object":{"state":1,"devpath":"\u0000","subsystem":"","env":{"V":"\ufffd"}}}} `}, `dispatch: rejecting coldboot loop: unique error 51966 injected by the test suite dispatch: processed inconsistent uevent: duplicate add event on devpath "\x00" `, 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)) }