G V-8 (A-3): the finding writer is generic over the contract's body types and works on their wire form
Writer[F, C, S, A any] marshals a body, reads and sets kind, scope, id, subject, at, since and observer by their contract names; the interfaces FindingBody, ClearedBody, StatedBody, AskedBody and the constructors go. A body whose wire form lacks a field the contract names is refused when the writer is made. views.CheckFinding(any) judges the wire form. WriterFindings takes only the writer. Builds: design 47 G §686, §640, §687, §689, A-3 (.work/47-G/dev2-ledger.md) Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
@@ -6,8 +6,8 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
)
|
||||
|
||||
// The test's own stand-in for the types the seat generator emits for the seat `mesh-conditions` (design 47 G §686,
|
||||
// §640): mesh-sdk keeps no such layout, so the writer's tests bring one, with the accessors the generator emits.
|
||||
// Plain structs shaped as the types the seat generator emits for the seat `mesh-conditions` (design 47 G A-3): the writer
|
||||
// is generic over them and works on their wire form, so the test brings the same shape, field for field.
|
||||
|
||||
type testSubject struct {
|
||||
Also []string `json:"also,omitempty"`
|
||||
@@ -17,52 +17,23 @@ type testSubject struct {
|
||||
}
|
||||
|
||||
type testFinding struct {
|
||||
AtVal time.Time `json:"at"`
|
||||
At time.Time `json:"at"`
|
||||
Explanation string `json:"explanation,omitempty"`
|
||||
HeadlineVal string `json:"headline,omitempty"`
|
||||
IDField string `json:"id"`
|
||||
KindField string `json:"kind"`
|
||||
ObserverVal string `json:"observer"`
|
||||
Headline string `json:"headline,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
Observer string `json:"observer"`
|
||||
Owner string `json:"owner,omitempty"`
|
||||
Run string `json:"run,omitempty"`
|
||||
ScopeField string `json:"scope"`
|
||||
SeverityVal string `json:"severity"`
|
||||
SinceVal time.Time `json:"since"`
|
||||
SourceVal string `json:"source"`
|
||||
SubjectVal testSubject `json:"subject"`
|
||||
SummaryVal string `json:"summary"`
|
||||
Scope string `json:"scope"`
|
||||
Severity string `json:"severity"`
|
||||
Since time.Time `json:"since"`
|
||||
Source string `json:"source"`
|
||||
Subject testSubject `json:"subject"`
|
||||
Summary string `json:"summary"`
|
||||
WaitingOn string `json:"waiting-on,omitempty"`
|
||||
}
|
||||
|
||||
var _ views.FindingBody = (*testFinding)(nil)
|
||||
|
||||
func (f *testFinding) Kind() string { return f.KindField }
|
||||
func (f *testFinding) SetKind(v string) { f.KindField = v }
|
||||
func (f *testFinding) Scope() string { return f.ScopeField }
|
||||
func (f *testFinding) SetScope(v string) { f.ScopeField = v }
|
||||
func (f *testFinding) ID() string { return f.IDField }
|
||||
func (f *testFinding) SetID(v string) { f.IDField = v }
|
||||
func (f *testFinding) At() time.Time { return f.AtVal }
|
||||
func (f *testFinding) Since() time.Time { return f.SinceVal }
|
||||
func (f *testFinding) SetSince(v time.Time) { f.SinceVal = v }
|
||||
func (f *testFinding) Severity() string { return f.SeverityVal }
|
||||
func (f *testFinding) SetSeverity(v string) { f.SeverityVal = v }
|
||||
func (f *testFinding) Summary() string { return f.SummaryVal }
|
||||
func (f *testFinding) SetSummary(v string) { f.SummaryVal = v }
|
||||
func (f *testFinding) Headline() string { return f.HeadlineVal }
|
||||
func (f *testFinding) SetHeadline(v string) { f.HeadlineVal = v }
|
||||
func (f *testFinding) Source() string { return f.SourceVal }
|
||||
func (f *testFinding) SetSource(v string) { f.SourceVal = v }
|
||||
func (f *testFinding) Observer() string { return f.ObserverVal }
|
||||
func (f *testFinding) SetObserver(v string) { f.ObserverVal = v }
|
||||
func (f *testFinding) SetAt(v time.Time) { f.AtVal = v }
|
||||
func (f *testFinding) SetSubject(a []string, id, m, s string) {
|
||||
f.SubjectVal = testSubject{Also: a, ID: id, Machine: m, Scope: s}
|
||||
}
|
||||
func (f *testFinding) Subject() ([]string, string, string, string) {
|
||||
return f.SubjectVal.Also, f.SubjectVal.ID, f.SubjectVal.Machine, f.SubjectVal.Scope
|
||||
}
|
||||
|
||||
type testCleared struct {
|
||||
At time.Time `json:"at"`
|
||||
ID string `json:"id"`
|
||||
@@ -71,12 +42,6 @@ type testCleared struct {
|
||||
Why string `json:"why"`
|
||||
}
|
||||
|
||||
func (c *testCleared) SetKind(v string) { c.Kind = v }
|
||||
func (c *testCleared) SetScope(v string) { c.Scope = v }
|
||||
func (c *testCleared) SetID(v string) { c.ID = v }
|
||||
func (c *testCleared) SetAt(v time.Time) { c.At = v }
|
||||
func (c *testCleared) SetWhy(v string) { c.Why = v }
|
||||
|
||||
type testPending struct {
|
||||
Bound string `json:"bound"`
|
||||
Key string `json:"key"`
|
||||
@@ -91,36 +56,11 @@ type testStated struct {
|
||||
Pending []testPending `json:"pending"`
|
||||
}
|
||||
|
||||
func newTestStated() *testStated {
|
||||
return &testStated{Findings: []testFinding{}, Pending: []testPending{}}
|
||||
}
|
||||
|
||||
func (s *testStated) SetAt(v time.Time) { s.At = v }
|
||||
func (s *testStated) SetAsked(v string) { s.Asked = v }
|
||||
func (s *testStated) SetKinds(v []string) { s.Kinds = v }
|
||||
func (s *testStated) AddFinding(b views.FindingBody) {
|
||||
s.Findings = append(s.Findings, *b.(*testFinding))
|
||||
}
|
||||
func (s *testStated) AddPending(k string, since time.Time, bound string) {
|
||||
s.Pending = append(s.Pending, testPending{Bound: bound, Key: k, Since: since})
|
||||
}
|
||||
|
||||
type testAsked struct {
|
||||
AskID string `json:"ask"`
|
||||
Mod string `json:"module,omitempty"`
|
||||
Ask string `json:"ask"`
|
||||
Module string `json:"module,omitempty"`
|
||||
}
|
||||
|
||||
func (a *testAsked) Ask() string { return a.AskID }
|
||||
func (a *testAsked) Module() string { return a.Mod }
|
||||
|
||||
var bodies = struct {
|
||||
NewFinding func() views.FindingBody
|
||||
NewCleared func() views.ClearedBody
|
||||
NewStated func() views.StatedBody
|
||||
NewAsked func() views.AskedBody
|
||||
}{
|
||||
func() views.FindingBody { return &testFinding{} },
|
||||
func() views.ClearedBody { return &testCleared{} },
|
||||
func() views.StatedBody { return newTestStated() },
|
||||
func() views.AskedBody { return &testAsked{} },
|
||||
}
|
||||
type testWriter = views.Writer[testFinding, testCleared, testStated, testAsked]
|
||||
type testConfig = views.WriterConfig[testFinding, testCleared, testStated, testAsked]
|
||||
type testObserved = views.Observed[testFinding]
|
||||
|
||||
+31
-23
@@ -619,43 +619,51 @@ 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"
|
||||
|
||||
// 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
|
||||
// through the body's setters, and the part supplies only New, the generated contract's constructor of a finding body:
|
||||
// `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.
|
||||
type WriterFindings struct {
|
||||
Writer *Writer
|
||||
New func() FindingBody
|
||||
// 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)
|
||||
}
|
||||
|
||||
func (o WriterFindings) body(f Finding) FindingBody {
|
||||
b := o.New()
|
||||
b.SetKind(f.Kind)
|
||||
b.SetScope(FindingScope)
|
||||
b.SetID(keys.EscapeName(f.ID))
|
||||
b.SetSeverity("warning")
|
||||
// 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 {
|
||||
b.SetSeverity("urgent")
|
||||
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
|
||||
}
|
||||
b.SetSource("views")
|
||||
b.SetSubject(nil, o.Writer.cfg.Module, o.Writer.cfg.Node, FindingScope)
|
||||
b.SetSummary(f.Summary)
|
||||
b.SetHeadline(f.Headline)
|
||||
if !f.At.IsZero() {
|
||||
b.SetAt(f.At)
|
||||
b["at"] = f.At
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// Put writes one finding.
|
||||
func (o WriterFindings) Put(f Finding) error {
|
||||
return o.Writer.Write(context.Background(), o.body(f))
|
||||
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(), b.Scope(), b.ID(), b.Kind(), "the part no longer observes it")
|
||||
return o.Writer.Clear(context.Background(), FindingScope, b["id"].(string), f.Kind, "the part no longer observes it")
|
||||
}
|
||||
|
||||
+167
-151
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"os"
|
||||
"regexp"
|
||||
"slices"
|
||||
@@ -31,67 +32,45 @@ const (
|
||||
ObserveRetryEvery = time.Minute
|
||||
)
|
||||
|
||||
// FindingBody is one finding as the seat `mesh-conditions` lays it out (design 47 G §686, §640): the writer keeps no copy
|
||||
// of that layout, it takes the type the seat generator emits through these accessors. A getter and a setter for each field
|
||||
// the writer reads or fills; the subject is one pair, its fields as plain values in the generated struct's order (design 47
|
||||
// G A-1). A body also marshals as JSON to its layout, which is how it is kept and said.
|
||||
type FindingBody interface {
|
||||
Kind() string
|
||||
SetKind(string)
|
||||
Scope() string
|
||||
SetScope(string)
|
||||
ID() string
|
||||
SetID(string)
|
||||
At() time.Time
|
||||
SetAt(time.Time)
|
||||
Since() time.Time
|
||||
SetSince(time.Time)
|
||||
Severity() string
|
||||
SetSeverity(string)
|
||||
Summary() string
|
||||
SetSummary(string)
|
||||
Headline() string
|
||||
SetHeadline(string)
|
||||
Source() string
|
||||
SetSource(string)
|
||||
Subject() (also []string, id, machine, scope string)
|
||||
SetSubject(also []string, id, machine, scope string)
|
||||
Observer() string
|
||||
SetObserver(string)
|
||||
// A body is handled in its wire form (design 47 G A-3): the writer is generic over the contract's body types, which stay plain
|
||||
// structs, and marshals a body, reads and sets `kind`, `scope`, `id`, `subject`, `at`, `since` and `observer` by their
|
||||
// contract names. wire is that form.
|
||||
type wire = map[string]any
|
||||
|
||||
// wireOf is v in its wire form.
|
||||
func wireOf(v any) (wire, error) {
|
||||
raw, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var m wire
|
||||
if err := json.Unmarshal(raw, &m); err != nil {
|
||||
return nil, fmt.Errorf("a body is not an object: %w", err)
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// ClearedBody is the body of `finding-cleared`, as the generated contract's type takes its fields (design 47 G A-1).
|
||||
type ClearedBody interface {
|
||||
SetKind(string)
|
||||
SetScope(string)
|
||||
SetID(string)
|
||||
SetAt(time.Time)
|
||||
SetWhy(string)
|
||||
}
|
||||
|
||||
// StatedBody is the body of `findings-stated`, as the generated contract's type takes its fields (design 47 G A-1):
|
||||
// every finding the observer observes now, and each key it cannot judge yet, with its since and its bound (a duration
|
||||
// such as 5m).
|
||||
type StatedBody interface {
|
||||
SetAt(time.Time)
|
||||
SetAsked(string)
|
||||
// SetKinds are the kinds the statement covers (design 47 G §580); none means every kind.
|
||||
SetKinds([]string)
|
||||
AddFinding(FindingBody)
|
||||
AddPending(key string, since time.Time, bound string)
|
||||
}
|
||||
|
||||
// AskedBody is the body of `findings-asked` the writer reads: the ask's id and the module asked, none asking every
|
||||
// observer (design 47 G A-1).
|
||||
type AskedBody interface {
|
||||
Ask() string
|
||||
Module() string
|
||||
}
|
||||
func str(m wire, field string) string { s, _ := m[field].(string); return s }
|
||||
|
||||
// FindingKey is the key of the finding of scope, id and kind: `<scope>.<id>.<kind>` (novox/hq ADR 0564 §2).
|
||||
func FindingKey(scope, id, kind string) string { return scope + "." + id + "." + kind }
|
||||
|
||||
func keyOf(b FindingBody) string { return FindingKey(b.Scope(), b.ID(), b.Kind()) }
|
||||
func keyOf(m wire) string { return FindingKey(str(m, "scope"), str(m, "id"), str(m, "kind")) }
|
||||
|
||||
// requireFields refuses a contract body whose wire form lacks a field the writer reads or sets (design 47 G A-3).
|
||||
func requireFields[T any](what string, fields ...string) error {
|
||||
var zero T
|
||||
m, err := wireOf(&zero)
|
||||
if err != nil {
|
||||
return fmt.Errorf("the %s body of the contract cannot be read: %w", what, err)
|
||||
}
|
||||
for _, f := range fields {
|
||||
if _, ok := m[f]; !ok {
|
||||
return fmt.Errorf("the %s body of the contract has no field %q on its wire form", what, f)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ViewKinds are the finding kinds a views reader writes through the part's writer (design 47 G §639).
|
||||
var ViewKinds = []string{ViewWaitsKind, UnansweredKind, UnsaidKind, TooLargeKind}
|
||||
@@ -119,8 +98,8 @@ type PendingKey struct {
|
||||
}
|
||||
|
||||
// Observed is what an observer's own truth says now: the findings it observes and the keys it cannot judge yet.
|
||||
type Observed struct {
|
||||
Findings []FindingBody
|
||||
type Observed[F any] struct {
|
||||
Findings []F
|
||||
Pending []PendingKey
|
||||
}
|
||||
|
||||
@@ -139,7 +118,7 @@ type WriterSubjects struct {
|
||||
}
|
||||
|
||||
// WriterConfig makes a Writer.
|
||||
type WriterConfig struct {
|
||||
type WriterConfig[F, C, S, A any] struct {
|
||||
// Module is the observer's module name: it fills the observer of a finding and tells an ask for another module.
|
||||
Module string
|
||||
// State is the observer's own state `findings`.
|
||||
@@ -153,18 +132,12 @@ type WriterConfig struct {
|
||||
Conn *nats.Conn
|
||||
// Subjects are the four events' subjects.
|
||||
Subjects WriterSubjects
|
||||
// NewFinding, NewCleared, NewStated and NewAsked make an empty body of the generated contract's types (design 47 G
|
||||
// §686, §640, A-1): the writer holds no layout of its own. All four are needed.
|
||||
NewFinding func() FindingBody
|
||||
NewCleared func() ClearedBody
|
||||
NewStated func() StatedBody
|
||||
NewAsked func() AskedBody
|
||||
// Node is the machine the module runs on, which a views reader's findings name in their subject (design 47 G §687);
|
||||
// none given, MESH_NODE.
|
||||
Node string
|
||||
// Observe answers the findings the observer observes now and its pending keys, or an error. It is called under the
|
||||
// writer's one lock, so it must not call Write or Clear.
|
||||
Observe func(ctx context.Context) (Observed, error)
|
||||
Observe func(ctx context.Context) (Observed[F], error)
|
||||
// Send puts a set larger than one bus message in the observer's sent store and answers what to publish instead
|
||||
// (stateobjects.Sender.Send); nil sends the body as it is.
|
||||
Send func(label string, body []byte) ([]byte, error)
|
||||
@@ -181,8 +154,8 @@ type WriterConfig struct {
|
||||
|
||||
// Writer keeps an observer's findings and says them (design 47 G §461, §485). Its acts, Write, Clear and a statement,
|
||||
// run one at a time in the order they are asked.
|
||||
type Writer struct {
|
||||
cfg WriterConfig
|
||||
type Writer[F, C, S, A any] struct {
|
||||
cfg WriterConfig[F, C, S, A]
|
||||
mu sync.Mutex // the one lock of the acts
|
||||
seq uint64
|
||||
ctl sync.Mutex // guards the consumer, the asks and the loop's context
|
||||
@@ -195,7 +168,7 @@ type Writer struct {
|
||||
}
|
||||
|
||||
// NewWriter is the writer for cfg.
|
||||
func NewWriter(cfg WriterConfig) (*Writer, error) {
|
||||
func NewWriter[F, C, S, A any](cfg WriterConfig[F, C, S, A]) (*Writer[F, C, S, A], error) {
|
||||
switch {
|
||||
case cfg.Module == "":
|
||||
return nil, errors.New("a finding writer needs its module's name")
|
||||
@@ -205,11 +178,21 @@ func NewWriter(cfg WriterConfig) (*Writer, error) {
|
||||
return nil, errors.New("a finding writer needs the observe function")
|
||||
case cfg.Subjects.Written == "" || cfg.Subjects.Cleared == "" || cfg.Subjects.Stated == "" || cfg.Subjects.Asked == "":
|
||||
return nil, errors.New("a finding writer needs the subjects of finding-written, finding-cleared, findings-stated and findings-asked")
|
||||
case cfg.NewFinding == nil || cfg.NewCleared == nil || cfg.NewStated == nil || cfg.NewAsked == nil:
|
||||
return nil, errors.New("a finding writer needs the generated contract's constructors of the finding, finding-cleared, findings-stated and findings-asked bodies")
|
||||
case cfg.JS == nil && (cfg.Publish == nil || cfg.Listen == nil):
|
||||
return nil, errors.New("a finding writer needs the bus connection's JetStream")
|
||||
}
|
||||
if err := requireFields[F]("finding", "kind", "scope", "id", "subject", "at", "since", "observer"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := requireFields[C]("finding-cleared", "kind", "scope", "id", "at", "why"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := requireFields[S]("findings-stated", "at", "findings", "pending"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := requireFields[A]("findings-asked", "ask"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cfg.Node == "" {
|
||||
cfg.Node = os.Getenv("MESH_NODE")
|
||||
}
|
||||
@@ -237,7 +220,7 @@ func NewWriter(cfg WriterConfig) (*Writer, error) {
|
||||
if cfg.Listen == nil {
|
||||
cfg.Listen = jetStreamListen(cfg.JS)
|
||||
}
|
||||
return &Writer{cfg: cfg, wake: make(chan struct{}, 1)}, nil
|
||||
return &Writer[F, C, S, A]{cfg: cfg, wake: make(chan struct{}, 1)}, nil
|
||||
}
|
||||
|
||||
func jetStreamPublish(js jetstream.JetStream) func(context.Context, string, []byte, string) error {
|
||||
@@ -274,7 +257,7 @@ func jetStreamListen(js jetstream.JetStream) func(context.Context, string, func(
|
||||
// Start makes the consumer of `findings-asked` and then states the findings now, and again on each ask and each time the
|
||||
// connection is made again (design 47 G §384, §485). It returns once the consumer exists; the statements run in the
|
||||
// writer's own goroutine until ctx ends or Close.
|
||||
func (w *Writer) Start(ctx context.Context) error {
|
||||
func (w *Writer[F, C, S, A]) Start(ctx context.Context) error {
|
||||
w.ctl.Lock()
|
||||
defer w.ctl.Unlock()
|
||||
if w.run != nil {
|
||||
@@ -302,7 +285,7 @@ func (w *Writer) Start(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// Close ends the writer's consumer and its statements.
|
||||
func (w *Writer) Close() {
|
||||
func (w *Writer[F, C, S, A]) Close() {
|
||||
w.ctl.Lock()
|
||||
defer w.ctl.Unlock()
|
||||
if w.stop != nil {
|
||||
@@ -314,7 +297,7 @@ func (w *Writer) Close() {
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Writer) listen() error {
|
||||
func (w *Writer[F, C, S, A]) listen() error {
|
||||
stop, err := w.cfg.Listen(w.run, w.cfg.Subjects.Asked, w.heard)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -328,7 +311,7 @@ func (w *Writer) listen() error {
|
||||
|
||||
// Reconnected is told that the bus connection was made again: the writer makes its consumer again, from the newest ask,
|
||||
// and states its findings again (design 47 G §485).
|
||||
func (w *Writer) Reconnected() {
|
||||
func (w *Writer[F, C, S, A]) Reconnected() {
|
||||
w.ctl.Lock()
|
||||
defer w.ctl.Unlock()
|
||||
if w.run == nil || w.run.Err() != nil {
|
||||
@@ -341,7 +324,7 @@ func (w *Writer) Reconnected() {
|
||||
w.signal()
|
||||
}
|
||||
|
||||
func (w *Writer) signal() {
|
||||
func (w *Writer[F, C, S, A]) signal() {
|
||||
select {
|
||||
case w.wake <- struct{}{}:
|
||||
default:
|
||||
@@ -349,22 +332,27 @@ func (w *Writer) signal() {
|
||||
}
|
||||
|
||||
// heard is an ask: answered when it names no module or this one.
|
||||
func (w *Writer) heard(data []byte) {
|
||||
a := w.cfg.NewAsked()
|
||||
if err := json.Unmarshal(data, a); err != nil {
|
||||
func (w *Writer[F, C, S, A]) heard(data []byte) {
|
||||
var body A
|
||||
if err := json.Unmarshal(data, &body); err != nil {
|
||||
w.cfg.Logf("an ask for findings could not be read: %v", err)
|
||||
return
|
||||
}
|
||||
if a.Module() != "" && a.Module() != w.cfg.Module {
|
||||
a, err := wireOf(&body)
|
||||
if err != nil {
|
||||
w.cfg.Logf("an ask for findings could not be read: %v", err)
|
||||
return
|
||||
}
|
||||
if str(a, "module") != "" && str(a, "module") != w.cfg.Module {
|
||||
return
|
||||
}
|
||||
w.ctl.Lock()
|
||||
w.asks, w.want = append(w.asks, a.Ask()), true
|
||||
w.asks, w.want = append(w.asks, str(a, "ask")), true
|
||||
w.ctl.Unlock()
|
||||
w.signal()
|
||||
}
|
||||
|
||||
func (w *Writer) loop(ctx context.Context) {
|
||||
func (w *Writer[F, C, S, A]) loop(ctx context.Context) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -397,12 +385,12 @@ func (w *Writer) loop(ctx context.Context) {
|
||||
|
||||
// owns says whether the writer's statement covers kind: every kind when none were given (design 47 G §580), else its
|
||||
// own and the views reader's.
|
||||
func (w *Writer) owns(kind string) bool {
|
||||
func (w *Writer[F, C, S, A]) owns(kind string) bool {
|
||||
return len(w.cfg.Kinds) == 0 || slices.Contains(w.cfg.Kinds, kind) || slices.Contains(ViewKinds, kind)
|
||||
}
|
||||
|
||||
// statedKinds are the kinds a statement names: none when the writer owns every kind.
|
||||
func (w *Writer) statedKinds() []string {
|
||||
func (w *Writer[F, C, S, A]) statedKinds() []string {
|
||||
if len(w.cfg.Kinds) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -412,7 +400,7 @@ func (w *Writer) statedKinds() []string {
|
||||
}
|
||||
|
||||
// ownsKey says whether key is one of a kind the writer owns: its last part is the kind (`<scope>.<id>.<kind>`).
|
||||
func (w *Writer) ownsKey(key string) bool {
|
||||
func (w *Writer[F, C, S, A]) ownsKey(key string) bool {
|
||||
if len(w.cfg.Kinds) == 0 {
|
||||
return true
|
||||
}
|
||||
@@ -420,20 +408,18 @@ func (w *Writer) ownsKey(key string) bool {
|
||||
return i >= 0 && w.owns(key[i+1:])
|
||||
}
|
||||
|
||||
func (w *Writer) now() time.Time { return w.cfg.Now().UTC() }
|
||||
func (w *Writer[F, C, S, A]) now() time.Time { return w.cfg.Now().UTC() }
|
||||
|
||||
// unsaid is a finding as compared to say whether what it says changed: not the time it was last seen, nor what saw it.
|
||||
func unsaid(f FindingBody) string {
|
||||
raw, _ := json.Marshal(f)
|
||||
var m map[string]any
|
||||
_ = json.Unmarshal(raw, &m)
|
||||
func unsaid(f wire) string {
|
||||
m := maps.Clone(f)
|
||||
delete(m, "at")
|
||||
delete(m, "run")
|
||||
raw, _ = json.Marshal(m)
|
||||
raw, _ := json.Marshal(m)
|
||||
return string(raw)
|
||||
}
|
||||
|
||||
func (w *Writer) read(key string) (FindingBody, error) {
|
||||
func (w *Writer[F, C, S, A]) read(key string) (wire, error) {
|
||||
e, err := w.cfg.State.Get(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -441,8 +427,8 @@ func (w *Writer) read(key string) (FindingBody, error) {
|
||||
if e == nil {
|
||||
return nil, nil
|
||||
}
|
||||
old := w.cfg.NewFinding()
|
||||
if err := json.Unmarshal(e.Value, old); err != nil {
|
||||
var old wire
|
||||
if err := json.Unmarshal(e.Value, &old); err != nil {
|
||||
return nil, fmt.Errorf("the finding %s in the state findings: %w", key, err)
|
||||
}
|
||||
return old, nil
|
||||
@@ -450,73 +436,88 @@ func (w *Writer) read(key string) (FindingBody, error) {
|
||||
|
||||
// CheckFinding refuses a finding the keeper could not make a condition of, naming the field of the first rule it breaks:
|
||||
// the key's three parts (`scope`, `id` and `kind`, each given, the kind with no dot, since the kind is the key's last
|
||||
// part), and the forms of `owner` and `waiting-on` (design 47 G §678, §689). The writer refuses such a finding before it
|
||||
// writes it, and the keeper judges the findings others write with it.
|
||||
func CheckFinding(b FindingBody) error {
|
||||
// part), and the forms of `owner` and `waiting-on` (design 47 G §678, §689). It judges the finding's wire form (design 47
|
||||
// G A-3): any value that marshals as a finding, the contract's generated type among them. The writer refuses such a
|
||||
// finding before it writes it, and the keeper judges the findings others write with it.
|
||||
func CheckFinding(finding any) error {
|
||||
m, ok := finding.(wire)
|
||||
if !ok {
|
||||
var err error
|
||||
if m, err = wireOf(finding); err != nil {
|
||||
return fmt.Errorf("the finding could not be read: %w", err)
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case strings.TrimSpace(b.Scope()) == "":
|
||||
case strings.TrimSpace(str(m, "scope")) == "":
|
||||
return errors.New("the finding's scope is not given")
|
||||
case strings.TrimSpace(b.ID()) == "":
|
||||
case strings.TrimSpace(str(m, "id")) == "":
|
||||
return errors.New("the finding's id is not given")
|
||||
case strings.TrimSpace(b.Kind()) == "":
|
||||
case strings.TrimSpace(str(m, "kind")) == "":
|
||||
return errors.New("the finding's kind is not given")
|
||||
case strings.Contains(b.Kind(), "."):
|
||||
return fmt.Errorf("the finding's kind %q holds a dot, and the kind is the last part of its key", b.Kind())
|
||||
case strings.Contains(str(m, "kind"), "."):
|
||||
return fmt.Errorf("the finding's kind %q holds a dot, and the kind is the last part of its key", str(m, "kind"))
|
||||
}
|
||||
raw, err := json.Marshal(b)
|
||||
if err != nil {
|
||||
return fmt.Errorf("the finding could not be read: %w", err)
|
||||
}
|
||||
var who struct {
|
||||
Owner string `json:"owner"`
|
||||
WaitingOn string `json:"waiting-on"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &who); err != nil {
|
||||
return fmt.Errorf("the finding could not be read: %w", err)
|
||||
}
|
||||
if err := formOfWho("owner", who.Owner); err != nil {
|
||||
if err := formOfWho("owner", str(m, "owner")); err != nil {
|
||||
return err
|
||||
}
|
||||
return formOfWho("waiting-on", who.WaitingOn)
|
||||
return formOfWho("waiting-on", str(m, "waiting-on"))
|
||||
}
|
||||
|
||||
// complete fills what the observer leaves to the writer and keeps the key's `since` where the key exists (design 47 G §384).
|
||||
func (w *Writer) complete(f FindingBody) (FindingBody, FindingBody, error) {
|
||||
func (w *Writer[F, C, S, A]) complete(finding F) (wire, wire, error) {
|
||||
f, err := wireOf(&finding)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("the finding could not be read: %w", err)
|
||||
}
|
||||
if err := CheckFinding(f); err != nil {
|
||||
return f, nil, err
|
||||
}
|
||||
if !w.owns(f.Kind()) {
|
||||
return f, nil, fmt.Errorf("the finding kind %q is not one this writer owns (%v)", f.Kind(), w.cfg.Kinds)
|
||||
if !w.owns(str(f, "kind")) {
|
||||
return f, nil, fmt.Errorf("the finding kind %q is not one this writer owns (%v)", str(f, "kind"), w.cfg.Kinds)
|
||||
}
|
||||
old, err := w.read(keyOf(f))
|
||||
if err != nil {
|
||||
return f, nil, err
|
||||
}
|
||||
if f.Observer() == "" {
|
||||
f.SetObserver(w.cfg.Module)
|
||||
if str(f, "observer") == "" {
|
||||
f["observer"] = w.cfg.Module
|
||||
}
|
||||
at := f.At()
|
||||
at, _ := time.Parse(time.RFC3339Nano, str(f, "at"))
|
||||
if at.IsZero() {
|
||||
at = w.now()
|
||||
}
|
||||
f.SetAt(at.UTC())
|
||||
since := f.Since()
|
||||
switch {
|
||||
case old != nil && !old.Since().IsZero():
|
||||
since = old.Since()
|
||||
case since.IsZero():
|
||||
since = f.At()
|
||||
f["at"] = at.UTC()
|
||||
since, _ := time.Parse(time.RFC3339Nano, str(f, "since"))
|
||||
if old != nil {
|
||||
if os, _ := time.Parse(time.RFC3339Nano, str(old, "since")); !os.IsZero() {
|
||||
since = os
|
||||
}
|
||||
}
|
||||
f.SetSince(since.UTC())
|
||||
if since.IsZero() {
|
||||
since = at
|
||||
}
|
||||
f["since"] = since.UTC()
|
||||
return f, old, nil
|
||||
}
|
||||
|
||||
// WriteWire is Write of a finding given in its wire form (a views reader's findings, design 47 G §687).
|
||||
func (w *Writer[F, C, S, A]) WriteWire(ctx context.Context, finding map[string]any) error {
|
||||
var f F
|
||||
if err := fillFrom(&f, finding); err != nil {
|
||||
return err
|
||||
}
|
||||
return w.Write(ctx, f)
|
||||
}
|
||||
|
||||
// ModuleNode is the module and the node the writer's findings are about.
|
||||
func (w *Writer[F, C, S, A]) ModuleNode() (string, string) { return w.cfg.Module, w.cfg.Node }
|
||||
|
||||
// Write keeps the finding in its key and says `finding-written` only when what the finding says changed (design 47 G
|
||||
// §383, §461). The key's `since` stays where the key exists.
|
||||
func (w *Writer) Write(ctx context.Context, f FindingBody) error {
|
||||
func (w *Writer[F, C, S, A]) Write(ctx context.Context, finding F) error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
f, old, err := w.complete(f)
|
||||
f, old, err := w.complete(finding)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -533,7 +534,7 @@ func (w *Writer) Write(ctx context.Context, f FindingBody) error {
|
||||
|
||||
// Clear deletes the finding's key and says `finding-cleared`; a key that does not exist is deleted of nothing and is said
|
||||
// all the same, so a keeper holding the finding clears it (design 47 G §485).
|
||||
func (w *Writer) Clear(ctx context.Context, scope, id, kind, why string) error {
|
||||
func (w *Writer[F, C, S, A]) Clear(ctx context.Context, scope, id, kind, why string) error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
key := FindingKey(scope, id, kind)
|
||||
@@ -543,13 +544,11 @@ func (w *Writer) Clear(ctx context.Context, scope, id, kind, why string) error {
|
||||
if err := w.cfg.State.Delete(key); err != nil {
|
||||
return fmt.Errorf("clearing the finding %s: %w", key, err)
|
||||
}
|
||||
cleared := w.cfg.NewCleared()
|
||||
cleared.SetKind(kind)
|
||||
cleared.SetScope(scope)
|
||||
cleared.SetID(id)
|
||||
cleared.SetAt(w.now())
|
||||
cleared.SetWhy(why)
|
||||
raw, _ := json.Marshal(cleared)
|
||||
var cleared C
|
||||
if err := fillFrom(&cleared, wire{"kind": kind, "scope": scope, "id": id, "at": w.now(), "why": why}); err != nil {
|
||||
return err
|
||||
}
|
||||
raw, _ := json.Marshal(&cleared)
|
||||
w.say(ctx, w.cfg.Subjects.Cleared, "finding-cleared", key, raw)
|
||||
return nil
|
||||
}
|
||||
@@ -557,7 +556,7 @@ func (w *Writer) Clear(ctx context.Context, scope, id, kind, why string) error {
|
||||
// State judges every finding anew with observe, writes each stated finding's key, deletes each key neither stated nor
|
||||
// pending, and says `findings-stated` (design 47 G §384, §461); asked is the ask it answers, none at a start. An error of
|
||||
// observe says no set.
|
||||
func (w *Writer) State(ctx context.Context, asked string) error {
|
||||
func (w *Writer[F, C, S, A]) State(ctx context.Context, asked string) error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
obs, err := w.cfg.Observe(ctx)
|
||||
@@ -565,12 +564,9 @@ func (w *Writer) State(ctx context.Context, asked string) error {
|
||||
return fmt.Errorf("observe: %w", err)
|
||||
}
|
||||
keep := map[string]bool{}
|
||||
body := w.cfg.NewStated()
|
||||
body.SetAt(w.now())
|
||||
body.SetAsked(asked)
|
||||
body.SetKinds(w.statedKinds())
|
||||
for _, f := range obs.Findings {
|
||||
f, _, err := w.complete(f)
|
||||
findings, pending := []wire{}, []wire{}
|
||||
for _, finding := range obs.Findings {
|
||||
f, _, err := w.complete(finding)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -578,11 +574,11 @@ func (w *Writer) State(ctx context.Context, asked string) error {
|
||||
return fmt.Errorf("writing the finding %s: %w", keyOf(f), err)
|
||||
}
|
||||
keep[keyOf(f)] = true
|
||||
body.AddFinding(f)
|
||||
findings = append(findings, f)
|
||||
}
|
||||
for _, p := range obs.Pending {
|
||||
keep[p.Key] = true
|
||||
body.AddPending(p.Key, p.Since, p.Bound)
|
||||
pending = append(pending, wire{"key": p.Key, "since": p.Since, "bound": p.Bound})
|
||||
}
|
||||
keys, err := w.cfg.State.Keys()
|
||||
if err != nil {
|
||||
@@ -594,7 +590,7 @@ func (w *Writer) State(ctx context.Context, asked string) error {
|
||||
if i := strings.LastIndex(k, "."); !keep[k] && i >= 0 && slices.Contains(ViewKinds, k[i+1:]) {
|
||||
if old, err := w.read(k); err == nil && old != nil {
|
||||
keep[k] = true
|
||||
body.AddFinding(old)
|
||||
findings = append(findings, old)
|
||||
}
|
||||
}
|
||||
if !keep[k] && w.ownsKey(k) {
|
||||
@@ -603,7 +599,18 @@ func (w *Writer) State(ctx context.Context, asked string) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
raw, _ := json.Marshal(body)
|
||||
stated := wire{"at": w.now(), "findings": findings, "pending": pending}
|
||||
if asked != "" {
|
||||
stated["asked"] = asked
|
||||
}
|
||||
if k := w.statedKinds(); k != nil {
|
||||
stated["kinds"] = k
|
||||
}
|
||||
var body S
|
||||
if err := fillFrom(&body, stated); err != nil {
|
||||
return err
|
||||
}
|
||||
raw, _ := json.Marshal(&body)
|
||||
if w.cfg.Send != nil {
|
||||
if raw, err = w.cfg.Send("findings-stated."+w.cfg.Module, raw); err != nil {
|
||||
w.cfg.Logf("the findings-stated of %s could not be sent, the next statement covers it: %v", w.cfg.Module, err)
|
||||
@@ -616,7 +623,7 @@ func (w *Writer) State(ctx context.Context, asked string) error {
|
||||
|
||||
// say publishes one event awaiting the bus's acknowledgement, sent again for PublishRetryFor when unacknowledged, then
|
||||
// written in the journal as an error naming what it said (design 47 G §383); the next statement covers it.
|
||||
func (w *Writer) say(ctx context.Context, subject, event, what string, data []byte) {
|
||||
func (w *Writer[F, C, S, A]) say(ctx context.Context, subject, event, what string, data []byte) {
|
||||
w.seq++
|
||||
id := fmt.Sprintf("%s-%s-%d-%d", w.cfg.Module, event, w.now().UnixNano(), w.seq)
|
||||
start := w.now()
|
||||
@@ -631,3 +638,12 @@ func (w *Writer) say(ctx context.Context, subject, event, what string, data []by
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fillFrom sets the contract body v from its wire form.
|
||||
func fillFrom(v any, m wire) error {
|
||||
raw, err := json.Marshal(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return json.Unmarshal(raw, v)
|
||||
}
|
||||
|
||||
@@ -43,11 +43,10 @@ func TestWriterSaysItsEventsAndHearsAsksOnTheBus(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
state := &fakeState{m: map[string][]byte{}}
|
||||
w, err := views.NewWriter(views.WriterConfig{Module: "m", State: state, JS: js, Conn: nc, Node: "n1",
|
||||
NewFinding: bodies.NewFinding, NewCleared: bodies.NewCleared, NewStated: bodies.NewStated, NewAsked: bodies.NewAsked,
|
||||
w, err := views.NewWriter(testConfig{Module: "m", State: state, JS: js, Conn: nc, Node: "n1",
|
||||
Subjects: views.WriterSubjects{Written: "obs.event.written", Cleared: "obs.event.cleared", Stated: "obs.event.stated", Asked: "obs.ask"},
|
||||
Observe: func(context.Context) (views.Observed, error) {
|
||||
return views.Observed{Findings: []views.FindingBody{finding("a", "now")}}, nil
|
||||
Observe: func(context.Context) (testObserved, error) {
|
||||
return testObserved{Findings: []testFinding{finding("a", "now")}}, nil
|
||||
}, Logf: t.Logf})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
+49
-33
@@ -67,11 +67,11 @@ type rig struct {
|
||||
fail int // publishes refused before one is taken
|
||||
waits []time.Duration
|
||||
logs []string
|
||||
observe func(context.Context) (views.Observed, error)
|
||||
observe func(context.Context) (testObserved, error)
|
||||
handle func([]byte)
|
||||
listens int
|
||||
stopped int
|
||||
w *views.Writer
|
||||
w *testWriter
|
||||
ch chan struct{}
|
||||
}
|
||||
|
||||
@@ -84,11 +84,10 @@ func newRig(t *testing.T) *rig { return newRigKinds(t) }
|
||||
|
||||
func newRigKinds(t *testing.T, kinds ...string) *rig {
|
||||
r := &rig{t: t, state: &fakeState{m: map[string][]byte{}}, now: time.Date(2026, 11, 11, 12, 0, 0, 0, time.UTC), ch: make(chan struct{}, 100)}
|
||||
r.observe = func(context.Context) (views.Observed, error) { return views.Observed{}, nil }
|
||||
w, err := views.NewWriter(views.WriterConfig{
|
||||
r.observe = func(context.Context) (testObserved, error) { return testObserved{}, nil }
|
||||
w, err := views.NewWriter(testConfig{
|
||||
Module: "m", State: r.state, Subjects: subjects, Kinds: kinds, Node: "n1",
|
||||
NewFinding: bodies.NewFinding, NewCleared: bodies.NewCleared, NewStated: bodies.NewStated, NewAsked: bodies.NewAsked,
|
||||
Observe: func(ctx context.Context) (views.Observed, error) { return r.observe(ctx) },
|
||||
Observe: func(ctx context.Context) (testObserved, error) { return r.observe(ctx) },
|
||||
Logf: func(f string, a ...any) {
|
||||
r.mu.Lock()
|
||||
r.logs = append(r.logs, fmt.Sprintf(f, a...))
|
||||
@@ -137,9 +136,9 @@ func (r *rig) events() []said {
|
||||
return slices.Clone(r.sent)
|
||||
}
|
||||
|
||||
func finding(id, summary string) *testFinding {
|
||||
return &testFinding{KindField: "k", ScopeField: "module", IDField: id, SeverityVal: "warning", SummaryVal: summary, SourceVal: "probe",
|
||||
SubjectVal: testSubject{Scope: "module", ID: id}}
|
||||
func finding(id, summary string) testFinding {
|
||||
return testFinding{Kind: "k", Scope: "module", ID: id, Severity: "warning", Summary: summary, Source: "probe",
|
||||
Subject: testSubject{Scope: "module", ID: id}}
|
||||
}
|
||||
|
||||
// A finding written says its event with the layout whole after the key, keeps the key's since, and says again only when
|
||||
@@ -168,7 +167,7 @@ func TestWriterWritesAndClearsFindingsAsEvents(t *testing.T) {
|
||||
if err := json.Unmarshal(ev[1].data, &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.SummaryVal != "two" || !got.SinceVal.Equal(first) || got.ObserverVal != "m" || views.FindingKey(got.ScopeField, got.IDField, got.KindField) != "module.a.k" || !got.AtVal.Equal(r.now) {
|
||||
if got.Summary != "two" || !got.Since.Equal(first) || got.Observer != "m" || views.FindingKey(got.Scope, got.ID, got.Kind) != "module.a.k" || !got.At.Equal(r.now) {
|
||||
t.Fatalf("body: %+v", got)
|
||||
}
|
||||
if e, _ := r.state.Get("module.a.k"); e == nil {
|
||||
@@ -222,14 +221,14 @@ func TestWriterStatesAtStartAndOnAsksAndRecovers(t *testing.T) {
|
||||
r.state.Put("module.old.k", finding("old", "x"))
|
||||
r.state.Put("module.wait.k", finding("wait", "x"))
|
||||
first := r.now
|
||||
r.state.Put("module.a.k", func() *testFinding { f := finding("a", "before"); f.SinceVal = first.Add(-time.Hour); return f }())
|
||||
r.state.Put("module.a.k", func() testFinding { f := finding("a", "before"); f.Since = first.Add(-time.Hour); return f }())
|
||||
calls := 0
|
||||
r.observe = func(context.Context) (views.Observed, error) {
|
||||
r.observe = func(context.Context) (testObserved, error) {
|
||||
calls++
|
||||
if calls <= 2 {
|
||||
return views.Observed{}, errors.New("not yet")
|
||||
return testObserved{}, errors.New("not yet")
|
||||
}
|
||||
return views.Observed{Findings: []views.FindingBody{finding("a", "now")},
|
||||
return testObserved{Findings: []testFinding{finding("a", "now")},
|
||||
Pending: []views.PendingKey{{Key: "module.wait.k", Since: first, Bound: "5m"}}}, nil
|
||||
}
|
||||
if err := r.w.Start(ctx); err != nil {
|
||||
@@ -244,8 +243,8 @@ func TestWriterStatesAtStartAndOnAsksAndRecovers(t *testing.T) {
|
||||
if err := json.Unmarshal(stated.data, &st); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if stated.subject != subjects.Stated || st.Asked != "" || len(st.Findings) != 1 || st.Findings[0].SummaryVal != "now" ||
|
||||
!st.Findings[0].SinceVal.Equal(first.Add(-time.Hour)) || len(st.Pending) != 1 || st.Pending[0].Key != "module.wait.k" {
|
||||
if stated.subject != subjects.Stated || st.Asked != "" || len(st.Findings) != 1 || st.Findings[0].Summary != "now" ||
|
||||
!st.Findings[0].Since.Equal(first.Add(-time.Hour)) || len(st.Pending) != 1 || st.Pending[0].Key != "module.wait.k" {
|
||||
t.Fatalf("statement: %s", stated.data)
|
||||
}
|
||||
keys, _ := r.state.Keys()
|
||||
@@ -291,10 +290,10 @@ func waitEvent(t *testing.T, r *rig, n int) said {
|
||||
func TestWriterRunsOneActAtATime(t *testing.T) {
|
||||
r := newRig(t)
|
||||
inside, release := make(chan struct{}), make(chan struct{})
|
||||
r.observe = func(context.Context) (views.Observed, error) {
|
||||
r.observe = func(context.Context) (testObserved, error) {
|
||||
close(inside)
|
||||
<-release
|
||||
return views.Observed{}, nil
|
||||
return testObserved{}, nil
|
||||
}
|
||||
done := make(chan error, 1)
|
||||
go func() { done <- r.w.State(context.Background(), "") }()
|
||||
@@ -323,8 +322,8 @@ func TestWriterKindsAndReaderFindings(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
r := newRigKinds(t, "k")
|
||||
other := finding("x", "another bundle's")
|
||||
other.KindField = "other"
|
||||
otherKey := views.FindingKey(other.ScopeField, other.IDField, other.KindField)
|
||||
other.Kind = "other"
|
||||
otherKey := views.FindingKey(other.Scope, other.ID, other.Kind)
|
||||
r.state.m[otherKey] = []byte(`{}`)
|
||||
if err := r.w.Write(ctx, other); err == nil || !strings.Contains(err.Error(), `"other"`) {
|
||||
t.Errorf("a write of another kind: %v", err)
|
||||
@@ -347,12 +346,12 @@ 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, New: bodies.NewFinding}
|
||||
rf := views.WriterFindings{Writer: r.w}
|
||||
if err := rf.Put(views.Finding{Kind: views.ViewWaitsKind, ID: "m.o.s", Summary: "waits"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r.observe = func(context.Context) (views.Observed, error) {
|
||||
return views.Observed{Findings: []views.FindingBody{finding("a", "observed")}}, nil
|
||||
r.observe = func(context.Context) (testObserved, error) {
|
||||
return testObserved{Findings: []testFinding{finding("a", "observed")}}, nil
|
||||
}
|
||||
if err := r.w.State(ctx, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -389,7 +388,7 @@ func TestWriterKindsAndReaderFindings(t *testing.T) {
|
||||
// (§689).
|
||||
func TestReaderFindingsAreLaidOutAndCheckFindingRefuses(t *testing.T) {
|
||||
r := newRig(t)
|
||||
rf := views.WriterFindings{Writer: r.w, New: bodies.NewFinding}
|
||||
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 {
|
||||
t.Fatal(err)
|
||||
@@ -403,21 +402,21 @@ func TestReaderFindingsAreLaidOutAndCheckFindingRefuses(t *testing.T) {
|
||||
if kind == views.TooLargeKind {
|
||||
sev = "urgent"
|
||||
}
|
||||
if got.KindField != kind || got.ScopeField != "module" || got.IDField != "o=2es=2emain" || got.SeverityVal != sev || got.SourceVal != "views" ||
|
||||
got.SubjectVal.Scope != "module" || got.SubjectVal.ID != "m" || got.SubjectVal.Machine != "n1" || got.SummaryVal != "sum "+kind || got.HeadlineVal != "head" || got.ObserverVal != "m" {
|
||||
if got.Kind != kind || got.Scope != "module" || got.ID != "o=2es=2emain" || got.Severity != sev || got.Source != "views" ||
|
||||
got.Subject.Scope != "module" || got.Subject.ID != "m" || got.Subject.Machine != "n1" || got.Summary != "sum "+kind || got.Headline != "head" || got.Observer != "m" {
|
||||
t.Errorf("%s laid out as %+v", kind, got)
|
||||
}
|
||||
}
|
||||
for name, c := range map[string]struct {
|
||||
f *testFinding
|
||||
f testFinding
|
||||
want string
|
||||
}{
|
||||
"scope": {&testFinding{IDField: "a", KindField: "k"}, "scope"},
|
||||
"id": {&testFinding{ScopeField: "module", KindField: "k"}, "id"},
|
||||
"kind": {&testFinding{ScopeField: "module", IDField: "a"}, "kind"},
|
||||
"dotted": {&testFinding{ScopeField: "module", IDField: "a", KindField: "k.x"}, "kind"},
|
||||
"owner": {&testFinding{ScopeField: "module", IDField: "a", KindField: "k", Owner: "Not A Form"}, "owner"},
|
||||
"waiting-on": {&testFinding{ScopeField: "module", IDField: "a", KindField: "k", WaitingOn: "x y"}, "waiting-on"},
|
||||
"scope": {testFinding{ID: "a", Kind: "k"}, "scope"},
|
||||
"id": {testFinding{Scope: "module", Kind: "k"}, "id"},
|
||||
"kind": {testFinding{Scope: "module", ID: "a"}, "kind"},
|
||||
"dotted": {testFinding{Scope: "module", ID: "a", Kind: "k.x"}, "kind"},
|
||||
"owner": {testFinding{Scope: "module", ID: "a", Kind: "k", Owner: "Not A Form"}, "owner"},
|
||||
"waiting-on": {testFinding{Scope: "module", ID: "a", Kind: "k", WaitingOn: "x y"}, "waiting-on"},
|
||||
} {
|
||||
if err := views.CheckFinding(c.f); err == nil || !strings.Contains(err.Error(), c.want) {
|
||||
t.Errorf("%s: %v", name, err)
|
||||
@@ -430,3 +429,20 @@ func TestReaderFindingsAreLaidOutAndCheckFindingRefuses(t *testing.T) {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
// A finding body whose wire form lacks a field the contract names is refused when the writer is made, naming the field
|
||||
// (design 47 G A-3).
|
||||
func TestWriterRefusesABodyWithoutAContractField(t *testing.T) {
|
||||
type noSince struct {
|
||||
Kind, Scope, ID, Observer string
|
||||
At time.Time `json:"at"`
|
||||
Subject testSubject
|
||||
}
|
||||
_, err := views.NewWriter(views.WriterConfig[noSince, testCleared, testStated, testAsked]{Module: "m", State: &fakeState{m: map[string][]byte{}},
|
||||
Subjects: subjects, Observe: func(context.Context) (views.Observed[noSince], error) { return views.Observed[noSince]{}, nil },
|
||||
Publish: func(context.Context, string, []byte, string) error { return nil },
|
||||
Listen: func(context.Context, string, func([]byte)) (func(), error) { return func() {}, nil }})
|
||||
if err == nil || !strings.Contains(err.Error(), `"kind"`) {
|
||||
t.Fatalf("a body of other wire names: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user