156 lines
5.3 KiB
Go
156 lines
5.3 KiB
Go
//go:build integration
|
|
|
|
package firstview
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/process"
|
|
"git.novox.be/novox/mesh-sdk/go/views"
|
|
"github.com/nats-io/nats.go"
|
|
|
|
"github.com/novox/mesh-tools/node-tools/internal/bus"
|
|
mt "github.com/novox/mesh-tools/node-tools/internal/meshtest"
|
|
)
|
|
|
|
// issue 1966 TLS-5 [i] (ADR 0615 §2, §4, §6): against the real bus, with the owners down and then up, the runner's own
|
|
// consumers on the owners' own views streams carry the manifest's four `consumes`: the first views are asked until answered,
|
|
// the unanswered ones are findings that the answer clears, and each owner's next event reaches its picture.
|
|
func TestTheRunnersConsumerCarriesTheOwnersViewsFamiliesWhenTheOwnersAreDownThenUp(t *testing.T) {
|
|
mesh := mt.New(t)
|
|
url := mt.URL(t)
|
|
|
|
// The owners publish their views on their own streams (views.Emit: `mesh.stream.<owner>.event.views.<state>`, on
|
|
// assigner-views and indexer-views), never on EVENTS. The consumers on them are the mesh's, one filter per family the
|
|
// manifest's `consumes` names, and the runner's membership names them as the mesh issues it (ADR 0363 §8).
|
|
raw, err := os.ReadFile("../../module.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var manifest struct {
|
|
Consumes []string `json:"consumes"`
|
|
}
|
|
if err := json.Unmarshal(raw, &manifest); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
streamOf := map[string]string{Assigner: catalogue.StreamBusName("assigner-views"), Indexer: catalogue.StreamBusName("indexer-views")}
|
|
filters := map[string][]string{}
|
|
families := 0
|
|
for _, c := range manifest.Consumes {
|
|
emitter, event, _ := strings.Cut(c, ".views.")
|
|
stream := streamOf[emitter]
|
|
if stream == "" {
|
|
t.Fatalf("the manifest consumes %s, whose views stream the test does not know", c)
|
|
}
|
|
filters[stream] = append(filters[stream], process.ModuleStreamSubject(emitter, views.EventPrefix+event))
|
|
families++
|
|
}
|
|
if families != len(Specs) {
|
|
t.Fatalf("the manifest consumes %d families, the runner holds %d first views", families, len(Specs))
|
|
}
|
|
runner := mt.MembershipOf("node-tools", "anchor", false, nil)
|
|
for _, owner := range []string{Assigner, Indexer} {
|
|
stream := streamOf[owner]
|
|
mesh.ModuleStream(t, owner, stream)
|
|
mesh.ConsumerOn(t, stream, "anchor", "node-tools", filters[stream], 2*time.Second)
|
|
runner.Hears = append(runner.Hears, bus.HeardIssued{Stream: stream, Consumer: "anchor_node-tools"})
|
|
}
|
|
mesh.Consumer(t, "anchor", "node-tools", []string{"mesh.mod.mesh-knowledge.event.reviewed"}, 2*time.Second) // its `waits`, on EVENTS
|
|
mesh.Issue(t, runner)
|
|
|
|
conn, err := bus.Connect(bus.Credential{URL: url, Module: "node-tools", Node: "anchor"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
conn.Logf = func(string, ...any) {}
|
|
t.Cleanup(conn.Close)
|
|
conn.Follow("node-tools")
|
|
mt.Until(t, func() error {
|
|
if conn.Membership("node-tools") == nil {
|
|
return errFindings(-1)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
var mu sync.Mutex
|
|
findings := map[string]bool{}
|
|
deps := DepsOf(conn)
|
|
deps.Bound, deps.Wait = 100*time.Millisecond, 20*time.Millisecond
|
|
deps.PutFinding = func(key string, _ json.RawMessage) error { mu.Lock(); findings[key] = true; mu.Unlock(); return nil }
|
|
deps.DeleteFinding = func(key string) error { mu.Lock(); delete(findings, key); mu.Unlock(); return nil }
|
|
v, stop := StartWith(context.Background(), deps, func(string, ...any) {})
|
|
t.Cleanup(stop)
|
|
|
|
// The owners are down: no tool answers, nothing is claimed, and past the bound each view is a finding.
|
|
if _, err := v.State(Assigner + ".placement"); err == nil {
|
|
t.Fatal("a state its owner has not answered is claimed")
|
|
}
|
|
mt.Until(t, func() error {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if len(findings) != len(Specs) {
|
|
return errFindings(len(findings))
|
|
}
|
|
return nil
|
|
})
|
|
|
|
// The owners come up and answer their first views.
|
|
nc, err := nats.Connect(url)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(nc.Close)
|
|
for _, s := range Specs {
|
|
s := s
|
|
if _, err := nc.Subscribe("mesh.mod."+s.Owner+".tool."+s.Tool, func(m *nats.Msg) {
|
|
answer, _ := json.Marshal(views.Answer{V: 1, Owner: s.Owner, State: s.State, Revision: 4, Created: time.Unix(1000, 0).UTC(),
|
|
Values: map[string]json.RawMessage{"first": json.RawMessage(`{"n":1}`)}})
|
|
body, _ := json.Marshal(map[string]json.RawMessage{"result": answer})
|
|
_ = m.Respond(body)
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
_ = nc.Flush()
|
|
for _, s := range Specs {
|
|
name := s.Name()
|
|
mt.Until(t, func() error { _, err := v.Entry(name, "first"); return err })
|
|
}
|
|
mt.Until(t, func() error {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if len(findings) != 0 {
|
|
return errFindings(len(findings))
|
|
}
|
|
return nil
|
|
})
|
|
|
|
// Each owner's next event arrives on the runner's consumer: all four families are bound.
|
|
for _, s := range Specs {
|
|
ev, _ := json.Marshal(views.Event{V: 1, Owner: s.Owner, State: s.State, Key: "next", Revision: 5, Created: time.Unix(1000, 0).UTC(),
|
|
Value: json.RawMessage(`{"n":2}`)})
|
|
mesh.Emit(t, process.ModuleStreamSubject(s.Owner, views.EventPrefix+s.State), json.RawMessage(ev))
|
|
}
|
|
for _, s := range Specs {
|
|
name := s.Name()
|
|
mt.Until(t, func() error {
|
|
got, err := v.Entry(name, "next")
|
|
if err != nil || string(got) != `{"n":2}` {
|
|
return errFindings(-1)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
}
|
|
|
|
type errFindings int
|
|
|
|
func (e errFindings) Error() string { return "not yet" }
|