From 948cc1b34e101c513631d94862e41103ba207593 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 21:57:46 +0100 Subject: [PATCH] G A-22, A-26: views.Finding, FindingWriter and WriterFindings are gone; the readers and the emitter lay out and write their findings through the part's generic writer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reader's four findings (unanswered, view-waits, unsaid, too-large) are laid out by mesh-sdk (design 47 G §687) on the wire form and written through views.WireWriter, which the generic Writer is; the emitter writes view-too-large itself and its TooLarge hook goes; Adopt wires every adopted follower's four callbacks to its reader. Builds: design 47 G §686, §687, §689, A-22, A-26 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ --- go/views/emit.go | 20 +++---- go/views/export_test.go | 11 ++++ go/views/findings.go | 115 ++++++++++++++++++++----------------- go/views/process_reader.go | 14 ++--- go/views/reader.go | 64 ++------------------- go/views/views_test.go | 90 +++++++++++++++++++---------- go/views/writer_test.go | 8 +-- 7 files changed, 159 insertions(+), 163 deletions(-) create mode 100644 go/views/export_test.go diff --git a/go/views/emit.go b/go/views/emit.go index 67ff31dd..1987ba0c 100644 --- a/go/views/emit.go +++ b/go/views/emit.go @@ -24,12 +24,10 @@ type Emitter struct { Publish func(ctx context.Context, msg *nats.Msg, id string) error // Marks is the owner's state MarksState; nil says nothing is marked, and every start says from the bucket's end. Marks jetstream.KeyValue - // TooLarge is told of a value too large for an event (said as a gap): the owner's finding naming the key. Nil - // only logs it. - TooLarge func(state, key string, size int) - // Findings writes and clears the owner's finding of a change not said past UnsaidAfter (UnsaidKind, its own state - // `findings`); nil says it in the log only. - Findings FindingWriter + // Findings is the owner's writer of its findings (its own state `findings`): the emitter writes and clears through it the + // finding of a change not said past UnsaidAfter (UnsaidKind) and writes that of a value too large for an event (TooLargeKind), + // laid out by mesh-sdk (design 47 G §687); nil says them in the log only. + Findings WireWriter // UnsaidAfter is how long a change may stay not said before it is the owner's finding; zero is Bound. UnsaidAfter time.Duration // Wait pauses before a change not said is said again (its attempt so far); it answers false when ctx ended. Nil is @@ -179,7 +177,7 @@ func (e Emitter) raiseUnsaid(state string, u jetstream.KeyValueEntry, first time if e.Findings == nil { return false } - if err := e.Findings.Put(UnsaidFinding(e.Owner, state, u.Key(), u.Revision(), first, cause.Error(), e.now())); err != nil { + if err := unsaidFinding(e.Owner, state, u.Key(), u.Revision(), first, cause.Error(), e.now()).put(e.Findings); err != nil { e.logf("views: the finding of %s's change not said could not be written: %v", state, err) return false } @@ -188,7 +186,7 @@ func (e Emitter) raiseUnsaid(state string, u jetstream.KeyValueEntry, first time // clearUnsaid clears the finding once the change is said; it answers whether it is cleared. func (e Emitter) clearUnsaid(state string) bool { - if err := e.Findings.Delete(UnsaidFinding(e.Owner, state, "", 0, time.Time{}, "", e.now())); err != nil { + if err := unsaidFinding(e.Owner, state, "", 0, time.Time{}, "", e.now()).drop(e.Findings); err != nil { e.logf("views: the finding of %s's change not said could not be cleared: %v", state, err) return false } @@ -226,8 +224,10 @@ func (e Emitter) Event(state string, made time.Time, u jetstream.KeyValueEntry) if n := len(u.Value()); n > MaxValueBytes { ev.Gap = true e.logf("views: %s.%s at %d holds %d bytes, more than an event carries: said as a gap, so a reader asks its first view", state, u.Key(), u.Revision(), n) - if e.TooLarge != nil { - e.TooLarge(state, u.Key(), n) + if e.Findings != nil { + if err := tooLargeFinding(e.Owner, state, u.Key(), n, e.now()).put(e.Findings); err != nil { + e.logf("views: the finding of %s.%s too large could not be written: %v", state, u.Key(), err) + } } return ev } diff --git a/go/views/export_test.go b/go/views/export_test.go new file mode 100644 index 00000000..7d586d5f --- /dev/null +++ b/go/views/export_test.go @@ -0,0 +1,11 @@ +package views + +// PutViewFinding and DropViewFinding write and clear one of a views reader's findings through w, laid out as mesh-sdk lays +// the reader's own (design 47 G §687), for the tests of package views_test. +func PutViewFinding(w WireWriter, kind, id, summary, headline string) error { + return viewFinding{kind: kind, id: id, summary: summary, headline: headline}.put(w) +} + +func DropViewFinding(w WireWriter, kind, id string) error { + return viewFinding{kind: kind, id: id}.drop(w) +} diff --git a/go/views/findings.go b/go/views/findings.go index a526f37b..2d1699c4 100644 --- a/go/views/findings.go +++ b/go/views/findings.go @@ -2,9 +2,12 @@ package views import ( "cmp" + "context" "fmt" "strings" "time" + + "git.novox.be/novox/mesh-sdk/go/keys" ) // The findings a views part writes in its own state `findings`, which the condition keeper raises and clears @@ -19,74 +22,80 @@ const ( ViewWaitsKind = "view-waits" ) -// Finding is what a views part says is wrong with a state it owns or follows, in plain fields: Kind and ID name it (a module-scoped -// finding of the observer), Summary, Headline and Explanation say it. It carries no layout: the writer a part gives -// (FindingWriter) encodes it as the condition keeper reads it, which is the seat mesh-conditions' contract, not this -// package's (novox/hq ADR 0634 §4, ADR 0635 §4). -type Finding struct { - Observer string - Kind string - ID string - Summary string - Headline string - Explanation string - At time.Time +// viewFinding is what a views part says is wrong with a state it owns or follows, in plain fields: kind and id name it, summary +// and headline say it. It is laid out in the wire form of the contract's finding by mesh-sdk (body, design 47 G §687, A-22) and +// written through the part's generic writer; there is no other form of it. +type viewFinding struct { + kind string + id string + summary string + headline string + at time.Time } -// FindingWriter writes and clears a part's findings in its own state `findings` (WriterFindings: through the part's mesh-sdk -// writer, design 47 G §639), where the condition keeper raises and -// clears conditions from them (novox/hq ADR 0564 §2): the part gives the one that encodes a Finding as that state's layout. -type FindingWriter interface { - Put(f Finding) error - Delete(f Finding) error +// WireWriter is what a views part's findings use of the part's finding writer, whatever the contract's body types: the +// generic Writer[F, C, S, A] is one (design 47 G §639, §687, A-22). +type WireWriter interface { + WriteWire(ctx context.Context, finding map[string]any) error + Clear(ctx context.Context, scope, id, kind, why string) error + ModuleNode() (module, node string) } -// UnsaidFinding is the owner's finding of a change of state it has not said on its views family since since: every -// reader of the state stands still at the change before it until it is said, which clears this. -func UnsaidFinding(owner, state, key string, revision uint64, since time.Time, cause string, at time.Time) Finding { - id := owner + "." + state +// FindingScope is the scope of a views reader's findings: they are findings of the part's module (design 47 G §687). +const FindingScope = "module" + +// body lays the finding out on its wire form (design 47 G §687): `kind` the finding's, `scope` module, `id` the finding's id +// escaped as a key's name is, `severity` warning but urgent for view-too-large, `source` views, `subject` the part's module on +// its node, `summary` and `headline` the finding's texts. +func (f viewFinding) body(module, node string) map[string]any { + sev := "warning" + if f.kind == TooLargeKind { + sev = "urgent" + } + subject := map[string]any{"scope": FindingScope, "id": module} + if node != "" { + subject["machine"] = node + } + b := map[string]any{"kind": f.kind, "scope": FindingScope, "id": keys.EscapeName(f.id), "severity": sev, "source": "views", + "subject": subject, "summary": f.summary} + if f.headline != "" { + b["headline"] = f.headline + } + if !f.at.IsZero() { + b["at"] = f.at + } + return b +} + +// put writes the finding through w; drop clears it. +func (f viewFinding) put(w WireWriter) error { + module, node := w.ModuleNode() + return w.WriteWire(context.Background(), f.body(module, node)) +} + +func (f viewFinding) drop(w WireWriter) error { + return w.Clear(context.Background(), FindingScope, keys.EscapeName(f.id), f.kind, "the part no longer observes it") +} + +func unsaidFinding(owner, state, key string, revision uint64, since time.Time, cause string, at time.Time) viewFinding { summary := fmt.Sprintf("%s has not said the change of %s.%s at %d on its views family since %s (%s): its readers stand still before it", owner, state, key, revision, since.UTC().Format(time.RFC3339), strings.TrimSpace(cause)) - return finding(owner, UnsaidKind, id, summary, - fmt.Sprintf("%s cannot say a change of its %s", owner, state), - "The owner says each change of its state as an event on its own stream, and says a change again until the bus takes it; "+ - "its readers hear nothing newer meanwhile. Look at the owner's stream on the bus: is it full, or is the bus away?", at) + return viewFinding{kind: UnsaidKind, id: owner + "." + state, summary: summary, headline: fmt.Sprintf("%s cannot say a change of its %s", owner, state), at: at.UTC()} } -// TooLargeFinding is the owner's finding of state's key too large to say whole on its views family. -func TooLargeFinding(owner, state, key string, size int, at time.Time) Finding { - id := owner + "." + state + "." + key +func tooLargeFinding(owner, state, key string, size int, at time.Time) viewFinding { summary := fmt.Sprintf("%s's %s.%s holds %d bytes, more than one views event carries (%d): every reader asks its first view again at each change of it", owner, state, key, size, MaxValueBytes) - return finding(owner, TooLargeKind, id, summary, - fmt.Sprintf("A value of %s is too large for its views event", owner), - "Each change of this key is said as a gap, so every reader of the state asks the owner's first view once more. "+ - "It works, but costs a first view per change; the value should be made smaller or split.", at) + return viewFinding{kind: TooLargeKind, id: owner + "." + state + "." + key, summary: summary, headline: fmt.Sprintf("A value of %s is too large for its views event", owner), at: at.UTC()} } -// UnansweredFinding is a reader's finding of its first view of s unanswered past its bound (ADR 0615 §6): it names the -// owner and the tool. A first view answered at revision 0 with nothing in it is said too, with the operator's step. -func UnansweredFinding(reader string, s Status, at time.Time) Finding { - id := reader + "." + s.Owner + "." + s.State +func unansweredFinding(reader string, s Status, at time.Time) viewFinding { summary := fmt.Sprintf("%s has no first view of %s's %s: %s has not answered since %s (%s)", reader, s.Owner, s.State, s.Tool, s.Asked.Format(time.RFC3339), strings.TrimSpace(s.Error)) - return finding(reader, UnansweredKind, id, summary, - fmt.Sprintf("%s waits for %s", reader, s.Owner), - fmt.Sprintf("%s reads %s's %s only through %s, and keeps asking it; it never reads the state itself. The answer clears this. "+ - "Look at %s's runtime of its own: is it running and connected?", reader, s.Owner, s.State, s.Tool, s.Owner), at) + return viewFinding{kind: UnansweredKind, id: reader + "." + s.Owner + "." + s.State, summary: summary, headline: fmt.Sprintf("%s waits for %s", reader, s.Owner), at: at.UTC()} } -func finding(observer, kind, id, summary, headline, explanation string, at time.Time) Finding { - return Finding{Observer: observer, Kind: kind, ID: id, Summary: summary, Headline: headline, Explanation: explanation, At: at.UTC()} -} - -// ViewWaitsFinding is a part's finding of the view it has waited for since s.WaitingSince (design 47): it names the -// part, the owner and the revision, and clears when the part holds it. -func ViewWaitsFinding(part string, s Status, at time.Time) Finding { - id := part + "." + s.Owner + "." + s.State +func viewWaitsFinding(part string, s Status, at time.Time) viewFinding { summary := fmt.Sprintf("%s waits for the view %s.%s at revision %s since %s and acts on nothing new until it holds it; it holds %s", part, s.Owner, s.State, s.WaitsFor, s.WaitingSince.UTC().Format(time.RFC3339), cmp.Or(s.View, "none")) - return finding(part, ViewWaitsKind, id, summary, - fmt.Sprintf("%s waits for a view of %s", part, s.Owner), - fmt.Sprintf("%s keeps running what it runs and takes up nothing new until %s's view %s reaches it. Look at %s: is it running, and has it "+ - "said that revision? %s's first views tool says what it holds and waits for.", part, s.Owner, s.WaitsFor, s.Owner, part), at) + return viewFinding{kind: ViewWaitsKind, id: part + "." + s.Owner + "." + s.State, summary: summary, headline: fmt.Sprintf("%s waits for a view of %s", part, s.Owner), at: at.UTC()} } diff --git a/go/views/process_reader.go b/go/views/process_reader.go index 6ba49939..5115032f 100644 --- a/go/views/process_reader.go +++ b/go/views/process_reader.go @@ -36,7 +36,7 @@ type ProcessReader struct { Call func(key string, body any) (json.RawMessage, error) // Findings is the reader's own state `findings`, where the condition keeper raises and clears the condition of a // first view unanswered past its bound (ADR 0615 §6); nil says it in the log only. - Findings func() FindingWriter + Findings func() WireWriter Logf func(string, ...any) // Now, Bound and Wait are for tests: zero are the clock, Bound and five seconds. Now func() time.Time @@ -250,12 +250,12 @@ func (r *ProcessReader) waits(s Status, raised bool) { if r.Now != nil { at = r.Now() } - f := ViewWaitsFinding(r.Module, s, at) + f := viewWaitsFinding(r.Module, s, at) var err error if raised { - err = kv.Put(f) + err = f.put(kv) } else { - err = kv.Delete(f) + err = f.drop(kv) } if err != nil { r.logf("views: the view-waits finding of %s is not written: %v", s.Tool, err) @@ -306,12 +306,12 @@ func (r *ProcessReader) finding(s Status, raised bool) { if r.Now != nil { at = r.Now() } - f := UnansweredFinding(r.Module, s, at) + f := unansweredFinding(r.Module, s, at) var err error if raised { - err = kv.Put(f) + err = f.put(kv) } else { - err = kv.Delete(f) + err = f.drop(kv) } if err != nil { r.logf("views: the finding of the first view %s is not written: %v", s.Tool, err) diff --git a/go/views/reader.go b/go/views/reader.go index e4616485..dc5b5f0b 100644 --- a/go/views/reader.go +++ b/go/views/reader.go @@ -26,7 +26,7 @@ type Reader struct { Module string // Findings writes and clears this module's finding of an unanswered first view (its own state `findings`); nil // says it in the log only. - Findings FindingWriter + Findings WireWriter Logf func(string, ...any) // Changed is told of each key a followed state changes, after the picture holds it (state is `.`). Changed func(state, key string, value json.RawMessage, deleted bool) @@ -225,12 +225,12 @@ func (r *Reader) waits(s Status, raised bool) { if r.Findings == nil { return } - f := ViewWaitsFinding(r.Module, s, r.nowTime()) + f := viewWaitsFinding(r.Module, s, r.nowTime()) var err error if raised { - err = r.Findings.Put(f) + err = f.put(r.Findings) } else { - err = r.Findings.Delete(f) + err = f.drop(r.Findings) } if err != nil { r.logf("the view-waits finding of %s is not written: %v", s.Tool, err) @@ -249,7 +249,7 @@ func (r *Reader) raise(s Status) { if r.Findings == nil { return } - if err := r.Findings.Put(UnansweredFinding(r.Module, s, r.nowTime())); err != nil { + if err := unansweredFinding(r.Module, s, r.nowTime()).put(r.Findings); err != nil { r.logf("the finding of the unanswered first view %s could not be written: %v", s.Tool, err) } } @@ -258,7 +258,7 @@ func (r *Reader) clear(s Status) { if r.Findings == nil { return } - if err := r.Findings.Delete(UnansweredFinding(r.Module, s, r.nowTime())); err != nil { + if err := unansweredFinding(r.Module, s, r.nowTime()).drop(r.Findings); err != nil { r.logf("the finding of the first view %s could not be cleared: %v", s.Tool, err) } } @@ -615,55 +615,3 @@ func (r *Reader) Tools(prefix string) []stdio.Tool { }}, } } - -// FindingScope is the scope of a views reader's findings: they are findings of the part's module (design 47 G §687). -const FindingScope = "module" - -// wireWriter is the part of a Writer[F, C, S, A] a views reader's findings use, whatever the contract's body types. -type wireWriter interface { - WriteWire(ctx context.Context, finding map[string]any) error - Clear(ctx context.Context, scope, id, kind, why string) error - ModuleNode() (module, node string) -} - -// WriterFindings writes a views reader's findings through the part's mesh-sdk writer, as kinds of the reader's own -// (design 47 G §639, §580, §687): the Findings of a reader, never a writer of its own. mesh-sdk lays each finding out on -// its wire form: `kind` the finding's, `scope` module, `id` the finding's id escaped as a key's name is, `severity` -// warning but urgent for view-too-large, `source` views, `subject` the part's module on its node, `summary` and -// `headline` the finding's texts. The writer's statement covers the reader's open findings beside its observe's. Writer -// is the part's *Writer[F, C, S, A]. -type WriterFindings struct { - Writer wireWriter -} - -func (o WriterFindings) body(f Finding) map[string]any { - module, node := o.Writer.ModuleNode() - sev := "warning" - if f.Kind == TooLargeKind { - sev = "urgent" - } - subject := map[string]any{"scope": FindingScope, "id": module} - if node != "" { - subject["machine"] = node - } - b := map[string]any{"kind": f.Kind, "scope": FindingScope, "id": keys.EscapeName(f.ID), "severity": sev, "source": "views", - "subject": subject, "summary": f.Summary} - if f.Headline != "" { - b["headline"] = f.Headline - } - if !f.At.IsZero() { - b["at"] = f.At - } - return b -} - -// Put writes one finding. -func (o WriterFindings) Put(f Finding) error { - return o.Writer.WriteWire(context.Background(), o.body(f)) -} - -// Delete clears one finding. -func (o WriterFindings) Delete(f Finding) error { - b := o.body(f) - return o.Writer.Clear(context.Background(), FindingScope, b["id"].(string), f.Kind, "the part no longer observes it") -} diff --git a/go/views/views_test.go b/go/views/views_test.go index 74cc5108..de02253a 100644 --- a/go/views/views_test.go +++ b/go/views/views_test.go @@ -17,6 +17,7 @@ import ( "github.com/nats-io/nats.go/jetstream" stdio "git.novox.be/novox/mesh-sdk/go" + "git.novox.be/novox/mesh-sdk/go/keys" "git.novox.be/novox/mesh-sdk/go/process" ) @@ -332,9 +333,9 @@ func TestViews(t *testing.T) { t.Errorf("raised = %+v", raised) } mu.Unlock() - fd := ViewWaitsFinding("mesh-delivery-builder", f.Status(), now()) - if fd.Kind != ViewWaitsKind || fd.ID != "mesh-delivery-builder.o.catalog.trunk" || !strings.Contains(fd.Summary, "waits for the view o.catalog.trunk at revision trunk.12") || - !strings.Contains(fd.Summary, "it holds trunk.11") { + fd := viewWaitsFinding("mesh-delivery-builder", f.Status(), now()) + if fd.kind != ViewWaitsKind || fd.id != "mesh-delivery-builder.o.catalog.trunk" || !strings.Contains(fd.summary, "waits for the view o.catalog.trunk at revision trunk.12") || + !strings.Contains(fd.summary, "it holds trunk.11") { t.Errorf("finding = %+v", fd) } none := &Follower{Owner: "o", Tool: "o.revision", State: BranchState("catalog", "new"), Now: now, @@ -688,7 +689,7 @@ func TestViews(t *testing.T) { kept := waitBound waitBound = func(context.Context, time.Duration) bool { return <-ended } defer func() { waitBound = kept }() - fs := &signalFindings{put: make(chan Finding, 4)} + fs := &signalFindings{put: make(chan seenFinding, 4)} r := &Reader{Module: "reader", Findings: fs, Now: now} var own []string f := &Follower{Owner: "s", Tool: "seat:s.revision", State: "revisions", Ask: answer, @@ -729,8 +730,8 @@ func TestViews(t *testing.T) { } // The process form: the picture reads the adopted follower, and the seat's event on the durable goes to the client. - pfs := &signalFindings{put: make(chan Finding, 4)} - pr := &ProcessReader{Module: "reader", Findings: func() FindingWriter { return pfs }, Now: now} + pfs := &signalFindings{put: make(chan seenFinding, 4)} + pr := &ProcessReader{Module: "reader", Findings: func() WireWriter { return pfs }, Now: now} pf := &Follower{Owner: "s", Tool: "seat:s.revision", State: "revisions", Ask: answer} p, err := pr.Adopt(pf, "s", "revision-said", hear) if err != nil || p.Bucket() != "s_revisions" || len(pr.Followers()) != 1 { @@ -818,7 +819,7 @@ func TestViews(t *testing.T) { heard[branch] = got return func(_ context.Context, d []byte) error { *got = append(*got, string(d)); return nil } } - fs := &signalFindings{put: make(chan Finding, 4)} + fs := &signalFindings{put: make(chan seenFinding, 4)} r := &Reader{Module: "reader", Findings: fs, Now: now} main := &Follower{Owner: "s", Tool: "seat:s.revision", State: BranchState("revisions", "main"), Ask: answer} if err := r.Adopt(main, "s", "revision-said", hearer("main")); err != nil { @@ -863,8 +864,8 @@ func TestViews(t *testing.T) { } // The process form: a view adopted after the start on another event is handled on the module's hub. - pfs := &signalFindings{put: make(chan Finding, 4)} - pr := &ProcessReader{Module: "reader", Findings: func() FindingWriter { return pfs }, Now: now, Hub: process.NewEventHub(t.Logf)} + pfs := &signalFindings{put: make(chan seenFinding, 4)} + pr := &ProcessReader{Module: "reader", Findings: func() WireWriter { return pfs }, Now: now, Hub: process.NewEventHub(t.Logf)} pm := &Follower{Owner: "s", Tool: "seat:s.revision", State: BranchState("revisions", "main"), Ask: answer} if _, err := pr.Adopt(pm, "s", "revision-said", hearer("pmain")); err != nil { t.Fatal(err) @@ -985,16 +986,16 @@ func TestViews(t *testing.T) { t.Errorf("%d failures: %d findings written, %d cleared, %d left, said %v", c.fails, fs.puts, fs.deletes, len(fs.m), said) } if c.puts > 0 { - f := UnsaidFinding("o", "s", "a", 1, made, "bus away", clock) - if f.Kind != UnsaidKind || f.ID != "o.s" || f.Observer != "o" || !strings.Contains(f.Summary, "o has not said the change of s.a at 1") { + f := unsaidFinding("o", "s", "a", 1, made, "bus away", clock) + if f.kind != UnsaidKind || f.id != "o.s" || !strings.Contains(f.summary, "o has not said the change of s.a at 1") { t.Errorf("the finding is %+v", f) } } } // A finding a run before a restart raised is cleared by the first change said after it. - left0 := UnsaidFinding("o", "s", "a", 1, made, "bus away", made) - key := left0.Kind + "/" + left0.ID - fs := &findingsMap{m: map[string]Finding{key: left0}} + left0 := unsaidFinding("o", "s", "a", 1, made, "bus away", made) + key := left0.kind + "/" + left0.id + fs := &findingsMap{m: map[string]seenFinding{key: {Kind: left0.kind, ID: left0.id}}} said, fails = nil, 0 em.Findings = fs updates = make(chan jetstream.KeyValueEntry, 1) @@ -1004,6 +1005,12 @@ func TestViews(t *testing.T) { if _, left := fs.m[key]; left || len(said) != 1 { t.Errorf("after a restart the leftover finding stands (%v), said %v", left, said) } + // A value too large for an event is said as a gap, and the emitter writes the owner's view-too-large itself (design 47 G §687). + tl := &findingsMap{} + te := Emitter{Owner: "o", Findings: tl, Now: func() time.Time { return made }} + if ev := te.Event("s", made, kvEntry{key: "big", rev: 3, at: made, size: MaxValueBytes + 1}); !ev.Gap || tl.m[TooLargeKind+"/o.s.big"].Kind != TooLargeKind { + t.Errorf("a value too large: gap %v, findings %v", ev.Gap, tl.m) + } }) t.Run("each change names the revision said before it, and the mark is said as an event once written", func(t *testing.T) { var said []Event @@ -1048,21 +1055,35 @@ func TestViews(t *testing.T) { }) } -// findingsMap is a module's own findings state in memory, keyed `/`. +// seenFinding is a finding a fake writer was handed, as its wire form names it. +type seenFinding struct{ Kind, ID, Summary string } + +// findingsMap is a module's own findings writer in memory, keyed `/` (the id as the finding names it). type findingsMap struct { - m map[string]Finding + m map[string]seenFinding puts, deletes int } -func (f *findingsMap) Put(v Finding) error { +func (f *findingsMap) ModuleNode() (string, string) { return "m", "n1" } + +func (f *findingsMap) WriteWire(_ context.Context, w map[string]any) error { + id, _ := keys.UnescapeName(w["id"].(string)) + v := seenFinding{Kind: w["kind"].(string), ID: id, Summary: w["summary"].(string)} if f.m == nil { - f.m = map[string]Finding{} + f.m = map[string]seenFinding{} } f.m[v.Kind+"/"+v.ID] = v f.puts++ return nil } +func (f *findingsMap) Clear(_ context.Context, _, id, kind, _ string) error { + plain, _ := keys.UnescapeName(id) + delete(f.m, kind+"/"+plain) + f.deletes++ + return nil +} + // marksKV is an owner's marks state that takes puts and nothing else. type marksKV struct { jetstream.KeyValue @@ -1074,25 +1095,28 @@ func (m *marksKV) Put(context.Context, string, []byte) (uint64, error) { return uint64(m.puts), nil } -func (f *findingsMap) Delete(v Finding) error { delete(f.m, v.Kind+"/"+v.ID); f.deletes++; return nil } - // signalFindings is a findingsMap that hands each finding written to put, once written. type signalFindings struct { findingsMap - put chan Finding + put chan seenFinding } -func (f *signalFindings) Put(v Finding) error { _ = f.findingsMap.Put(v); f.put <- v; return nil } +func (f *signalFindings) WriteWire(ctx context.Context, w map[string]any) error { + _ = f.findingsMap.WriteWire(ctx, w) + id, _ := keys.UnescapeName(w["id"].(string)) + f.put <- seenFinding{Kind: w["kind"].(string), ID: id, Summary: w["summary"].(string)} + return nil +} // next is the next finding written; none within a second fails the test (a bound, never waited out when it comes). -func (f *signalFindings) next(t *testing.T) Finding { +func (f *signalFindings) next(t *testing.T) seenFinding { t.Helper() select { case v := <-f.put: return v case <-time.After(time.Second): t.Fatal("no finding was written") - return Finding{} + return seenFinding{} } } @@ -1125,14 +1149,20 @@ func (f actsFunc) RecordHandAct(a HandAct) error { return f(a) } // kvEntry is one change of a bucket as a watch hands it. type kvEntry struct { - key string - rev uint64 - at time.Time + key string + rev uint64 + at time.Time + size int // the value's length when over the one event's: zero is a small value } -func (k kvEntry) Bucket() string { return "b" } -func (k kvEntry) Key() string { return k.key } -func (k kvEntry) Value() []byte { return []byte(`{"n":1}`) } +func (k kvEntry) Bucket() string { return "b" } +func (k kvEntry) Key() string { return k.key } +func (k kvEntry) Value() []byte { + if k.size > 0 { + return make([]byte, k.size) + } + return []byte(`{"n":1}`) +} func (k kvEntry) Revision() uint64 { return k.rev } func (k kvEntry) Created() time.Time { return k.at } func (k kvEntry) Delta() uint64 { return 0 } diff --git a/go/views/writer_test.go b/go/views/writer_test.go index f071d22e..45b5ee53 100644 --- a/go/views/writer_test.go +++ b/go/views/writer_test.go @@ -354,8 +354,7 @@ func TestWriterKindsAndReaderFindings(t *testing.T) { } } // a views reader writes through the part's writer, and the statement keeps its open findings - rf := views.WriterFindings{Writer: r.w} - if err := rf.Put(views.Finding{Kind: views.ViewWaitsKind, ID: "m.o.s", Summary: "waits"}); err != nil { + if err := views.PutViewFinding(r.w, views.ViewWaitsKind, "m.o.s", "waits", ""); err != nil { t.Fatal(err) } r.observe = func(context.Context) (testObserved, error) { @@ -382,7 +381,7 @@ func TestWriterKindsAndReaderFindings(t *testing.T) { if !slices.Contains(stated.Kinds, "k") || !slices.Contains(stated.Kinds, views.ViewWaitsKind) || len(stated.Findings) != 2 { t.Errorf("the statement: kinds %v, %d findings", stated.Kinds, len(stated.Findings)) } - if err := rf.Delete(views.Finding{Kind: views.ViewWaitsKind, ID: "m.o.s"}); err != nil { + if err := views.DropViewFinding(r.w, views.ViewWaitsKind, "m.o.s"); err != nil { t.Fatal(err) } if _, ok := r.state.m["module.m=2eo=2es.view-waits"]; ok { @@ -396,9 +395,8 @@ func TestWriterKindsAndReaderFindings(t *testing.T) { // (§689). func TestReaderFindingsAreLaidOutAndCheckFindingRefuses(t *testing.T) { r := newRig(t) - rf := views.WriterFindings{Writer: r.w} for _, kind := range views.ViewKinds { - if err := rf.Put(views.Finding{Kind: kind, ID: "o.s.main", Summary: "sum " + kind, Headline: "head"}); err != nil { + if err := views.PutViewFinding(r.w, kind, "o.s.main", "sum "+kind, "head"); err != nil { t.Fatal(err) } e, _ := r.state.Get("module.o=2es=2emain." + kind)