From caf3e0db4b92d859599ee199b32051fae8c99016 Mon Sep 17 00:00:00 2001 From: Ophestra Date: Tue, 26 May 2026 18:06:07 +0900 Subject: [PATCH] internal/kobject: improve error JSON representation This is now usable by internal/report. Signed-off-by: Ophestra --- internal/kobject/kobject.go | 188 +++++++++++++++++-------------- internal/kobject/kobject_test.go | 168 ++++++++++++++++++--------- internal/report/report_test.go | 25 +++- 3 files changed, 238 insertions(+), 143 deletions(-) diff --git a/internal/kobject/kobject.go b/internal/kobject/kobject.go index 72d8f052..26359114 100644 --- a/internal/kobject/kobject.go +++ b/internal/kobject/kobject.go @@ -9,6 +9,7 @@ import ( "strconv" "sync" + "hakurei.app/internal/report" "hakurei.app/internal/uevent" ) @@ -29,9 +30,9 @@ const ( // Object represents a kernel object. type Object struct { // Origin of the object. - State int `json:"-"` + State int `json:"state,omitempty"` // Set by [uevent.KOBJ_OFFLINE] and [uevent.KOBJ_ONLINE]. - Offline bool + Offline bool `json:"offline,omitempty"` // alloc_uevent_skb: devpath DevPath string `json:"devpath"` @@ -48,11 +49,11 @@ type Object struct { Env map[string]string `json:"env"` } -// Clone returns a copy of o. -func (o *Object) Clone() Object { +// Clone returns the address of a copy of o. +func (o *Object) Clone() *Object { v := *o v.Env = maps.Clone(o.Env) - return v + return &v } // GoString returns compound literal for the underlying value. @@ -194,77 +195,94 @@ func (s *State) Range(ctx context.Context, f func(o *Object) bool) { } } -// UnexpectedColdbootError is reported by [State.Consume] for a coldboot event -// with action other than the expected [uevent.KOBJ_ADD]. -type UnexpectedColdbootError Event - -func (e UnexpectedColdbootError) Error() string { - return "unexpected " + e.Action.String() + " coldboot event" +// An EventError describes a malformed or inconsistent [Event]. +type EventError struct { + Kind int `json:"fault"` + E Event `json:"event"` + O *Object `json:"object,omitempty"` } -// DuplicateAddError is reported by [State.Consume] for a [uevent.KOBJ_ADD] -// event on a still-existing entry that was not the result of a coldboot. -type DuplicateAddError Event +var _ report.RepresentableError = EventError{} -func (e DuplicateAddError) Error() string { - return "duplicate add event on devpath " + strconv.Quote(e.DevPath) -} +func (EventError) Representable() {} -// TargetError is reported by [State.Consume] for an event on a nonexistent -// entry. This is generally only possible before coldboot completes. -type TargetError Event +const ( + // EUnexpectedColdboot is reported for a coldboot event with action other + // than the expected [uevent.KOBJ_ADD]. + EUnexpectedColdboot = iota + // EDuplicateAdd is reported for a [uevent.KOBJ_ADD] event on a + // still-existing entry that was not the result of a coldboot. + EDuplicateAdd + // EBadTarget is reported for an event on a nonexistent [Object]. This is + // generally only possible before coldboot completes. + EBadTarget + // ERemoveState is reported for a [uevent.KOBJ_REMOVE] event targeting an + // entry in a state other than [StateColdboot] and [StateNew]. + ERemoveState + // EUnexpectedOffline is reported for a [uevent.KOBJ_OFFLINE] or + // [uevent.KOBJ_ONLINE] event targeting an already offline or online object. + EUnexpectedOffline + // EBindState is reported for a [uevent.KOBJ_BIND] event targeting an entry + // in a state other than [StateColdboot] and [StateNew]. + EBindState + // EUnbindState is reported for a [uevent.KOBJ_UNBIND] event targeting an + // entry in a state other than [StateBound]. + EUnbindState + // EMalformedMove is reported for a [uevent.KOBJ_MOVE] event missing the + // DEVPATH_OLD environment variable. + EMalformedMove +) -func (e TargetError) Error() string { - return "unexpected " + e.Action.String() + - " event on devpath " + strconv.Quote(e.DevPath) -} +func (e EventError) Error() string { + switch e.Kind { + case EUnexpectedColdboot: + return "unexpected " + e.E.Action.String() + " coldboot event" + case EDuplicateAdd: + return "duplicate add event on devpath " + strconv.Quote(e.E.DevPath) + case EBadTarget: + return "unexpected " + e.E.Action.String() + " event on devpath " + + strconv.Quote(e.E.DevPath) + case ERemoveState: + if e.O == nil { + return "invalid remove event error" + } + return "remove event targeting devpath " + strconv.Quote(e.E.DevPath) + + " in state " + strconv.Itoa(e.O.State) + case EUnexpectedOffline: + if e.O == nil { + return "invalid unexpected offline error" + } + if e.O.Offline { + return "offline event targeting devpath " + strconv.Quote(e.E.DevPath) + } + return "online event targeting devpath " + strconv.Quote(e.E.DevPath) + case EBindState: + if e.O == nil { + return "invalid bind state error" + } + return "bind event targeting devpath " + strconv.Quote(e.E.DevPath) + + " in state " + strconv.Itoa(e.O.State) + case EUnbindState: + if e.O == nil { + return "invalid unbind state error" + } + return "unbind event targeting devpath " + strconv.Quote(e.E.DevPath) + + " in state " + strconv.Itoa(e.O.State) + case EMalformedMove: + return "move event targeting devpath " + strconv.Quote(e.E.DevPath) + + " missing DEVPATH_OLD" -// RemoveStateError is reported by [State.Consume] for a [uevent.KOBJ_REMOVE] -// event targeting an entry in a state other than [StateColdboot] and [StateNew]. -type RemoveStateError Object - -func (e RemoveStateError) Error() string { - return "remove event targeting devpath " + strconv.Quote(e.DevPath) + - " in state " + strconv.Itoa(e.State) -} - -// BindStateError is reported by [State.Consume] for a [uevent.KOBJ_BIND] event -// targeting an entry in a state other than [StateColdboot] and [StateNew]. -type BindStateError Object - -func (e BindStateError) Error() string { - return "bind event targeting devpath " + strconv.Quote(e.DevPath) + - " in state " + strconv.Itoa(e.State) -} - -// UnbindStateError is reported by [State.Consume] for a [uevent.KOBJ_UNBIND] -// event targeting an entry in a state other than [StateBound]. -type UnbindStateError Object - -func (e UnbindStateError) Error() string { - return "unbind event targeting devpath " + strconv.Quote(e.DevPath) + - " in state " + strconv.Itoa(e.State) -} - -// MalformedMoveError is reported by [State.Consume] for a [uevent.KOBJ_MOVE] -// event missing the DEVPATH_OLD environment variable. -type MalformedMoveError Event - -func (e MalformedMoveError) Error() string { - return "move event targeting devpath " + strconv.Quote(e.DevPath) + - " missing DEVPATH_OLD" -} - -// UnexpectedOfflineError is reported by [State.Consume] for a -// [uevent.KOBJ_OFFLINE] or [uevent.KOBJ_ONLINE] event targeting an already -// offline or online object. -type UnexpectedOfflineError Object - -func (e UnexpectedOfflineError) Error() string { - if e.Offline { - return "offline event targeting devpath " + strconv.Quote(e.DevPath) + default: + return "invalid event error kind " + strconv.Itoa(e.Kind) } - return "online event targeting devpath " + strconv.Quote(e.DevPath) +} + +// NewError returns a new [EventError] for e and o. +func (e *Event) NewError(kind int, o *Object) error { + if o != nil { + o = o.Clone() + } + return EventError{kind, e.Clone(), o} } // processEvent merges an event into s. @@ -274,7 +292,7 @@ func (s *State) processEvent(e *Event) { coldboot := e.Synth != nil if e.Action != uevent.KOBJ_ADD && coldboot { - s.reportErr(UnexpectedColdbootError(e.Clone())) + s.reportErr(e.NewError(EUnexpectedColdboot, nil)) return } @@ -282,7 +300,7 @@ func (s *State) processEvent(e *Event) { case uevent.KOBJ_ADD: if e.Synth == nil { if o, ok := s.uevent[e.DevPath]; ok { - s.reportErr(DuplicateAddError(e.Clone())) + s.reportErr(e.NewError(EDuplicateAdd, o)) o.merge(e.Env) s.dispatchIter(o) return @@ -299,10 +317,10 @@ func (s *State) processEvent(e *Event) { case uevent.KOBJ_REMOVE: if o, ok := s.uevent[e.DevPath]; !ok { - s.reportErr(TargetError(e.Clone())) + s.reportErr(e.NewError(EBadTarget, nil)) return } else if o.State != StateColdboot && o.State != StateNew { - s.reportErr(RemoveStateError(o.Clone())) + s.reportErr(e.NewError(ERemoveState, o)) } delete(s.uevent, e.DevPath) return @@ -310,7 +328,7 @@ func (s *State) processEvent(e *Event) { case uevent.KOBJ_CHANGE: o, ok := s.uevent[e.DevPath] if !ok { - s.reportErr(TargetError(e.Clone())) + s.reportErr(e.NewError(EBadTarget, nil)) // this suffers from the coldboot race window similar to KOBJ_MOVE, // however this action combines driver-specific and change-specific // environment variables and combines them with environment @@ -333,11 +351,11 @@ func (s *State) processEvent(e *Event) { case uevent.KOBJ_MOVE: var o *Object if old, ok := e.Env["DEVPATH_OLD"]; !ok { - s.reportErr(MalformedMoveError(e.Clone())) + s.reportErr(e.NewError(EMalformedMove, nil)) // not reached o = e.makeColdboot() } else if o, ok = s.uevent[old]; !ok { - s.reportErr(TargetError(e.Clone())) + s.reportErr(e.NewError(EBadTarget, nil)) // this generally happens during coldboot, dropping the event here // may cause inconsistent state if the coldboot event for this // object was generated before the bind event @@ -356,14 +374,14 @@ func (s *State) processEvent(e *Event) { case uevent.KOBJ_ONLINE: o, ok := s.uevent[e.DevPath] if !ok { - s.reportErr(TargetError(e.Clone())) + s.reportErr(e.NewError(EBadTarget, nil)) // coldboot race window similar to an unexpected KOBJ_MOVE o = e.makeColdboot() s.uevent[e.DevPath] = o o.merge(e.Env) } if !o.Offline { - s.reportErr(UnexpectedOfflineError(o.Clone())) + s.reportErr(e.NewError(EUnexpectedOffline, o)) } o.Offline = false s.dispatchIter(o) @@ -372,14 +390,14 @@ func (s *State) processEvent(e *Event) { case uevent.KOBJ_OFFLINE: o, ok := s.uevent[e.DevPath] if !ok { - s.reportErr(TargetError(e.Clone())) + s.reportErr(e.NewError(EBadTarget, nil)) // coldboot race window similar to an unexpected KOBJ_MOVE o = e.makeColdboot() s.uevent[e.DevPath] = o o.merge(e.Env) } if o.Offline { - s.reportErr(UnexpectedOfflineError(o.Clone())) + s.reportErr(e.NewError(EUnexpectedOffline, o)) } o.Offline = true s.dispatchIter(o) @@ -388,13 +406,13 @@ func (s *State) processEvent(e *Event) { case uevent.KOBJ_BIND: o, ok := s.uevent[e.DevPath] if !ok { - s.reportErr(TargetError(e.Clone())) + s.reportErr(e.NewError(EBadTarget, nil)) // coldboot race window similar to an unexpected KOBJ_MOVE o = e.makeColdboot() s.uevent[e.DevPath] = o } if o.State != StateColdboot && o.State != StateNew { - s.reportErr(BindStateError(o.Clone())) + s.reportErr(e.NewError(EBindState, o)) } o.State = StateBound o.merge(e.Env) @@ -404,13 +422,13 @@ func (s *State) processEvent(e *Event) { case uevent.KOBJ_UNBIND: o, ok := s.uevent[e.DevPath] if !ok { - s.reportErr(TargetError(e.Clone())) + s.reportErr(e.NewError(EBadTarget, nil)) // coldboot race window similar to an unexpected KOBJ_MOVE, but does // not result in inconsistent state if dropped return } if o.State != StateBound { - s.reportErr(UnbindStateError(o.Clone())) + s.reportErr(e.NewError(EUnbindState, o)) } o.State = StateNew o.Driver = "" @@ -423,7 +441,7 @@ func (s *State) processEvent(e *Event) { } } -// BadSequenceError is reported by [State.Consume] for an unexpected SEQNUM. +// BadSequenceError is reported for an unexpected SEQNUM. type BadSequenceError struct{ Got, Want uint64 } func (e BadSequenceError) Error() string { diff --git a/internal/kobject/kobject_test.go b/internal/kobject/kobject_test.go index f04db2d1..b184e621 100644 --- a/internal/kobject/kobject_test.go +++ b/internal/kobject/kobject_test.go @@ -42,12 +42,12 @@ func TestConsume(t *testing.T) { "SYNTH_UUID=fdfdfdfd-fdfd-fdfd-fdfd-fdfdfdfdfdfd", }}, }, map[string]*Object{}, nil, []error{ - UnexpectedColdbootError{ + (&Event{ Action: uevent.KOBJ_BIND, DevPath: "\x00", Env: map[string]string{}, Synth: &coldboot, - }, + }).NewError(EUnexpectedColdboot, nil), }}, {"sequence", []*uevent.Message{ @@ -85,12 +85,16 @@ func TestConsume(t *testing.T) { Env: map[string]string{"V": "\xfd"}, }, }, nil, []error{ - DuplicateAddError{ + (&Event{ Action: uevent.KOBJ_ADD, DevPath: "\x00", Env: map[string]string{"V": "\xfd"}, Sequence: 2, - }, + }).NewError(EDuplicateAdd, &Object{ + State: StateNew, + DevPath: "\x00", + Env: map[string]string{"V": "\xff"}, + }), }}, {"remove", []*uevent.Message{ @@ -109,16 +113,21 @@ func TestConsume(t *testing.T) { "SEQNUM=3", }}, }, map[string]*Object{}, nil, []error{ - TargetError{ + (&Event{ Action: uevent.KOBJ_REMOVE, DevPath: "\x00", Env: map[string]string{}, - }, - RemoveStateError{ + }).NewError(EBadTarget, nil), + (&Event{ + Action: uevent.KOBJ_REMOVE, + DevPath: "\x00", + Env: map[string]string{}, + Sequence: 3, + }).NewError(ERemoveState, &Object{ State: StateBound, DevPath: "\x00", Driver: "\xfd", - }, + }), }}, {"change", []*uevent.Message{ @@ -133,14 +142,14 @@ func TestConsume(t *testing.T) { Env: map[string]string{"V": "\xff"}, }, }, nil, []error{ - TargetError{ + (&Event{ Action: uevent.KOBJ_CHANGE, DevPath: "\x00", Sequence: 9, Env: map[string]string{ "V": "\xff", }, - }, + }).NewError(EBadTarget, nil), }}, {"move", []*uevent.Message{ @@ -157,14 +166,14 @@ func TestConsume(t *testing.T) { "\x0f": {DevPath: "\x0f", Env: map[string]string{"V": "\xfc"}}, "\x0e": {DevPath: "\x0e", Env: map[string]string{"V": "\xfd"}}, }, nil, []error{ - MalformedMoveError{ + (&Event{ Action: uevent.KOBJ_MOVE, DevPath: "\x0f", Env: map[string]string{ "V": "\xfc", }, - }, - TargetError{ + }).NewError(EMalformedMove, nil), + (&Event{ Action: uevent.KOBJ_MOVE, DevPath: "\x0e", Env: map[string]string{ @@ -172,7 +181,7 @@ func TestConsume(t *testing.T) { "DEVPATH_OLD": "\xff", }, Sequence: 1, - }, + }).NewError(EBadTarget, nil), }}, {"offline", []*uevent.Message{ @@ -203,31 +212,43 @@ func TestConsume(t *testing.T) { Env: map[string]string{"V": "\xf0"}, }, }, nil, []error{ - TargetError{ + (&Event{ Action: uevent.KOBJ_ONLINE, DevPath: "\xfd", Env: map[string]string{ "V": "\xff", }, Sequence: 9, - }, - UnexpectedOfflineError{ + }).NewError(EBadTarget, nil), + (&Event{ + Action: uevent.KOBJ_ONLINE, DevPath: "\xfd", Env: map[string]string{ "V": "\xff", }, - }, - UnexpectedOfflineError{ + Sequence: 9, + }).NewError(EUnexpectedOffline, &Object{ + DevPath: "\xfd", + Env: map[string]string{ + "V": "\xff", + }, + }), + (&Event{ + Action: uevent.KOBJ_OFFLINE, + DevPath: "\xfd", + Env: map[string]string{}, + Sequence: 11, + }).NewError(EUnexpectedOffline, &Object{ Offline: true, DevPath: "\xfd", Env: map[string]string{"V": "\xff"}, - }, - TargetError{ + }), + (&Event{ Action: uevent.KOBJ_OFFLINE, DevPath: "\xfc", Env: map[string]string{"V": "\xf0"}, Sequence: 12, - }, + }).NewError(EBadTarget, nil), }}, {"bind", []*uevent.Message{ @@ -272,22 +293,32 @@ func TestConsume(t *testing.T) { "V": "\xf0", }, }}, nil, []error{ - UnbindStateError{ + (&Event{ + Action: uevent.KOBJ_UNBIND, + DevPath: "\x00", + Env: map[string]string{"_V": "\xfd"}, + Sequence: 2, + }).NewError(EUnbindState, &Object{ State: StateNew, DevPath: "\x00", Env: map[string]string{"V": "\xff"}, - }, - BindStateError{ + }), + (&Event{ + Action: uevent.KOBJ_BIND, + DevPath: "\x00", + Env: map[string]string{"___V": "\xfb"}, + Sequence: 4, + }).NewError(EBindState, &Object{ State: StateBound, DevPath: "\x00", Env: map[string]string{"V": "\xff", "__V": "\xfc"}, - }, - TargetError{ + }), + (&Event{ Action: uevent.KOBJ_BIND, DevPath: "\f", Env: map[string]string{"V": "\xf0", "DRIVER": "\x01"}, Sequence: 5, - }, + }).NewError(EBadTarget, nil), }}, {"unbind", []*uevent.Message{ @@ -295,12 +326,12 @@ func TestConsume(t *testing.T) { "SEQNUM=9", }}, }, map[string]*Object{}, nil, []error{ - TargetError{ + (&Event{ Action: uevent.KOBJ_UNBIND, DevPath: "\xfd", Env: map[string]string{}, Sequence: 9, - }, + }).NewError(EBadTarget, nil), }}, } for _, tc := range testCases { @@ -375,7 +406,7 @@ func TestIter(t *testing.T) { var done bool wg.Go(func() { s.Range(ctx, func(o *Object) bool { - got = append(got, new(o.Clone())) + got = append(got, o.Clone()) return !done }) }) @@ -435,46 +466,75 @@ func TestErrors(t *testing.T) { Want: 0xbabe, }, "SEQNUM=51966, want 47806"}, - {"UnexpectedColdbootError", UnexpectedColdbootError{ + {"EUnexpectedColdboot", (&Event{ Action: 0xbeef, - }, "unexpected unsupported kobject_action 48879 coldboot event"}, + }).NewError(EUnexpectedColdboot, nil), + "unexpected unsupported kobject_action 48879 coldboot event"}, - {"DuplicateAddError", DuplicateAddError{ + {"EDuplicateAdd", (&Event{ Action: uevent.KOBJ_ADD, DevPath: "\x00", - }, `duplicate add event on devpath "\x00"`}, + }).NewError(EDuplicateAdd, nil), + `duplicate add event on devpath "\x00"`}, - {"TargetError", TargetError{ + {"EBadTarget", (&Event{ Action: uevent.KOBJ_UNBIND, DevPath: "\x00", - }, `unexpected unbind event on devpath "\x00"`}, + }).NewError(EBadTarget, nil), + `unexpected unbind event on devpath "\x00"`}, - {"RemoveStateError", RemoveStateError{ - State: StateBound, + {"ERemoveState", (&Event{ DevPath: "\x00", - }, `remove event targeting devpath "\x00" in state 2`}, + }).NewError(ERemoveState, &Object{ + State: StateBound, + }), `remove event targeting devpath "\x00" in state 2`}, + {"ERemoveState invalid", (&Event{ + DevPath: "\x00", + }).NewError(ERemoveState, nil), + "invalid remove event error"}, - {"BindStateError", BindStateError{ - State: StateBound, + {"EBindState", (&Event{ DevPath: "\x00", - }, `bind event targeting devpath "\x00" in state 2`}, + }).NewError(EBindState, &Object{ + State: StateBound, + }), `bind event targeting devpath "\x00" in state 2`}, + {"EBindState invalid", (&Event{ + DevPath: "\x00", + }).NewError(EBindState, nil), + "invalid bind state error"}, - {"UnbindStateError", UnbindStateError{ - State: StateNew, + {"EUnbindState", (&Event{ DevPath: "\x00", - }, `unbind event targeting devpath "\x00" in state 1`}, + }).NewError(EUnbindState, &Object{ + State: StateNew, + }), `unbind event targeting devpath "\x00" in state 1`}, + {"EUnbindState invalid", (&Event{ + DevPath: "\x00", + }).NewError(EUnbindState, nil), + "invalid unbind state error"}, - {"MalformedMoveError", MalformedMoveError{ + {"EMalformedMove", (&Event{ DevPath: "\x00", - }, `move event targeting devpath "\x00" missing DEVPATH_OLD`}, + }).NewError(EMalformedMove, nil), + `move event targeting devpath "\x00" missing DEVPATH_OLD`}, - {"UnexpectedOfflineError", UnexpectedOfflineError{ + {"EUnexpectedOffline", (&Event{ DevPath: "\x00", - }, `online event targeting devpath "\x00"`}, - {"UnexpectedOfflineError offline", UnexpectedOfflineError{ + }).NewError(EUnexpectedOffline, &Object{ + Offline: false, + }), `online event targeting devpath "\x00"`}, + {"EUnexpectedOffline offline", (&Event{ DevPath: "\x00", + }).NewError(EUnexpectedOffline, &Object{ Offline: true, - }, `offline event targeting devpath "\x00"`}, + }), `offline event targeting devpath "\x00"`}, + {"EUnexpectedOffline invalid", (&Event{ + DevPath: "\x00", + }).NewError(EUnexpectedOffline, nil), + "invalid unexpected offline error"}, + + {"EventError invalid", &EventError{Kind: 0xbad}, + "invalid event error kind 2989"}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { @@ -1648,13 +1708,13 @@ func TestConsumeSample(t *testing.T) { delete(want, "/devices/virtual/net/lo") o.DevPath = "/devices/virtual/net/_lo" o.Env["INTERFACE"] = "_lo" - want["/devices/virtual/net/_lo"] = &o + want["/devices/virtual/net/_lo"] = o }, nil}, {"cpu", testdata02, func(want map[string]*Object) { o := want["/devices/system/machinecheck/machinecheck0"].Clone() o.State = StateNew - want["/devices/system/machinecheck/machinecheck0"] = &o + want["/devices/system/machinecheck/machinecheck0"] = o }, nil}, {"loop", testdata03, func(want map[string]*Object) { diff --git a/internal/report/report_test.go b/internal/report/report_test.go index 307968b4..6c7a4046 100644 --- a/internal/report/report_test.go +++ b/internal/report/report_test.go @@ -11,8 +11,10 @@ import ( "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 } @@ -38,10 +40,25 @@ func TestDispatch(t *testing.T) { 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}, + { + 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{ {