The Go runtime answers $SRV.PING, $SRV.INFO and $SRV.STATS (and per name and id) in the io.nats.micro.v1 format with what it serves at the moment it is asked: one service per runtime process, since the bus admits one reply per request from each responder, and one endpoint per tool per subject, its metadata saying module, seat, scope, machine, description, schema and whether the module is interchangeable. Serving is unchanged. The console gathers one $SRV.INFO request's answers instead of asking the catalogue's roster and each module's tools, and reads the controller's records as JSON for what should have answered: an assignment with tools that did not announce is named, a module without tools never is. The text parsers of node list and module list are gone. Packages share the test bus: go test -p 1.
114 lines
3.9 KiB
Go
114 lines
3.9 KiB
Go
package runtime
|
|
|
|
import (
|
|
"encoding/json"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
"github.com/nats-io/nats.go/micro"
|
|
|
|
"github.com/novox/mesh-tools/node-tools/internal/bus"
|
|
mt "github.com/novox/mesh-tools/node-tools/internal/meshtest"
|
|
)
|
|
|
|
// novox/hq ADR 0197: what a runtime serves, it announces — the NATS services protocol's discovery,
|
|
// read here with NATS's own types, each endpoint a subject actually served — and the announcement
|
|
// follows a re-issued membership.
|
|
func TestTheRuntimeAnnouncesWhatItServesInTheServicesProtocol(t *testing.T) {
|
|
mesh := mt.New(t)
|
|
mesh.Issue(t, mt.MembershipOf("alpha", "anchor", false, nil))
|
|
mesh.Issue(t, mt.MembershipOf("beta", "anchor", false, map[string][]string{"node-shelf": {"list", "clear"}}))
|
|
nodeTools := connect(t, "node-tools", "anchor")
|
|
stop, err := Run(nodeTools, []Served{
|
|
{"alpha", []string{mt.Fixture("many-alpha.serve.mjs")}},
|
|
{"beta", []string{mt.Fixture("many-beta.serve.mjs")}},
|
|
}, nil, (&mt.Logs{}).Logf)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer stop()
|
|
|
|
nc, err := nats.Connect(mt.URL(t))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer nc.Close()
|
|
info := func() micro.Info {
|
|
t.Helper()
|
|
msg, err := nc.Request("$SRV.INFO.node-tools.anchor", nil, 2*time.Second)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var i micro.Info
|
|
if err := json.Unmarshal(msg.Data, &i); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return i
|
|
}
|
|
subjects := func(i micro.Info) string {
|
|
var out []string
|
|
for _, e := range i.Endpoints {
|
|
out = append(out, e.Subject+"|"+e.QueueGroup+"|"+e.Metadata["kind"]+"|"+e.Metadata["module"]+"|"+e.Metadata["seat"]+"|"+e.Metadata["scope"]+"|"+e.Metadata["interchangeable"])
|
|
}
|
|
sort.Strings(out)
|
|
return strings.Join(out, "\n")
|
|
}
|
|
|
|
got := info()
|
|
if got.Type != micro.InfoResponseType || got.Name != "node-tools" || got.ID != "anchor" || got.Version == "" {
|
|
t.Errorf("identity: %+v", got.ServiceIdentity)
|
|
}
|
|
want := strings.Join([]string{
|
|
"mesh.mod.alpha.tool.one.anchor||tool|alpha|||false",
|
|
"mesh.mod.alpha.tool.two.anchor||tool|alpha|||false",
|
|
"mesh.mod.beta.tool.five.anchor||tool|beta|||false",
|
|
"mesh.mod.beta.tool.four.anchor||tool|beta|||false",
|
|
"mesh.mod.beta.tool.three.anchor||tool|beta|||false",
|
|
"mesh.seat.node-shelf.tool.clear.anchor||seat|beta|node-shelf|node|false",
|
|
"mesh.seat.node-shelf.tool.list.anchor||seat|beta|node-shelf|node|false",
|
|
}, "\n")
|
|
if s := subjects(got); s != want {
|
|
t.Errorf("announced:\n%s\nwant:\n%s", s, want)
|
|
}
|
|
for _, e := range got.Endpoints {
|
|
if e.Metadata["node"] != "anchor" || e.Metadata["description"] == "" || !json.Valid([]byte(e.Metadata["schema"])) {
|
|
t.Errorf("endpoint metadata: %+v", e)
|
|
}
|
|
}
|
|
|
|
// Every subject announced is answered.
|
|
asker := connect(t, "console", "workstation")
|
|
for _, e := range got.Endpoints {
|
|
if _, err := asker.Ask("", map[string]any{}, e.Subject); err != nil {
|
|
t.Errorf("announced %s and does not answer it: %v", e.Subject, err)
|
|
}
|
|
}
|
|
|
|
// PING answers with the same identity, and a request for another service is not answered.
|
|
if msg, err := nc.Request("$SRV.PING", nil, 2*time.Second); err != nil || !strings.Contains(string(msg.Data), micro.PingResponseType) {
|
|
t.Errorf("ping: %v %v", msg, err)
|
|
}
|
|
if _, err := nc.Request("$SRV.INFO.somebody-else", nil, 300*time.Millisecond); err == nil {
|
|
t.Error("answered a request for another service")
|
|
}
|
|
|
|
// alpha is re-issued a plain subject: the announcement says so, and says it is interchangeable.
|
|
mesh.Issue(t, mt.MembershipOf("alpha", "anchor", true, nil))
|
|
var after string
|
|
for i := 0; i < 40; i++ {
|
|
after = subjects(info())
|
|
if strings.Contains(after, "mesh.mod.alpha.tool.one|serve.alpha|tool|alpha|||true") {
|
|
break
|
|
}
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
if !strings.Contains(after, "mesh.mod.alpha.tool.one|serve.alpha|tool|alpha|||true") ||
|
|
!strings.Contains(after, "mesh.mod.alpha.tool.one.anchor||tool|alpha|||true") {
|
|
t.Errorf("after a re-issued membership:\n%s", after)
|
|
}
|
|
_ = bus.Served{}
|
|
}
|