Merge pull request 'Console: a seat and a module of one name are each reached (hq issue 287)' (#19) from fix/a-seat-and-a-module-of-one-name into main
This commit was merged in pull request #19.
This commit is contained in:
@@ -26,9 +26,12 @@ fi
|
||||
CGO_ENABLED=0 go vet ./...
|
||||
|
||||
packages=$(go list ./...)
|
||||
bundleless=""
|
||||
if ! command -v node >/dev/null 2>&1 || [ ! -d node_modules/@novox/mesh-sdk ]; then
|
||||
echo "NOT TESTED HERE: internal/runtime and internal/console launch bundles that need node and @novox/mesh-sdk"
|
||||
packages=$(printf '%s\n' "$packages" | grep -v -e '/internal/runtime$' -e '/internal/console$')
|
||||
# What of the console launches no bundle runs anyway: how an address resolves (novox/hq issue 287).
|
||||
bundleless='^TestASeatAndAModuleOfOneNameAreEachReached$'
|
||||
fi
|
||||
if command -v gcc >/dev/null 2>&1; then
|
||||
CGO_ENABLED=1 go test -race -count=1 $packages
|
||||
@@ -36,4 +39,7 @@ else
|
||||
echo "NOT RACE-CHECKED: the toolchain holds no C compiler; the suite runs without the race detector"
|
||||
CGO_ENABLED=0 go test -count=1 $packages
|
||||
fi
|
||||
if [ -n "$bundleless" ]; then
|
||||
CGO_ENABLED=0 go test -count=1 -run "$bundleless" ./internal/console
|
||||
fi
|
||||
echo "NOT TESTED HERE: node-tools/src (TypeScript) needs @novox/mesh-sdk from the package registry"
|
||||
|
||||
@@ -243,100 +243,17 @@ func indexOn(conn *bus.Conn) (*index, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l := &Listing{Tools: []Tool{}, NotAnswering: []string{}}
|
||||
x := &index{Modules: map[string]*moduleInfo{}, Listing: l, Discovery: d, Recorded: map[string][]string{}}
|
||||
var endpoints []announce.Endpoint
|
||||
for _, h := range d.Heard {
|
||||
endpoints = append(endpoints, announce.Endpoints(h.Info)...)
|
||||
}
|
||||
x, announced, machines := indexOfEndpoints(endpoints)
|
||||
l := x.Listing
|
||||
x.Discovery = d
|
||||
for _, s := range d.Silent {
|
||||
x.Unheard = append(x.Unheard, unheard{Runtime: s.Name, Machine: s.Machine,
|
||||
Why: fmt.Sprintf("it answered PING; what it serves did not arrive within %s", announce.Patience)})
|
||||
}
|
||||
announced := map[string]map[string]bool{} // module → node → announced something
|
||||
seats := map[string]*seatInfo{}
|
||||
toolAt := map[string]int{} // <module>.<tool> or <seat>.<verb> → index in l.Tools
|
||||
machines := map[string]bool{}
|
||||
for _, h := range d.Heard {
|
||||
for _, e := range announce.Endpoints(h.Info) {
|
||||
if e.Node != "" {
|
||||
machines[e.Node] = true
|
||||
}
|
||||
if announced[e.Module] == nil {
|
||||
announced[e.Module] = map[string]bool{}
|
||||
}
|
||||
announced[e.Module][e.Node] = true
|
||||
switch e.Kind {
|
||||
case announce.KindSeat:
|
||||
st := seats[e.Seat]
|
||||
if st == nil {
|
||||
st = &seatInfo{Seat: e.Seat, Scope: e.Scope}
|
||||
seats[e.Seat] = st
|
||||
}
|
||||
if st.Scope != "node" && e.Scope == "node" {
|
||||
st.Scope = "node"
|
||||
}
|
||||
h := holder{Module: e.Module, Node: e.Node}
|
||||
if !containsHolder(st.Holders, h) {
|
||||
st.Holders = append(st.Holders, h)
|
||||
}
|
||||
key := e.Seat + "." + e.Tool
|
||||
if _, have := toolAt[key]; !have {
|
||||
toolAt[key] = len(l.Tools)
|
||||
t := Tool{Module: e.Seat, Name: e.Tool, Description: e.Description, Input: e.Schema, Seat: true, Scope: st.Scope}
|
||||
l.Tools = append(l.Tools, t)
|
||||
st.Verbs = append(st.Verbs, t)
|
||||
}
|
||||
case announce.KindTool:
|
||||
m := x.Modules[e.Module]
|
||||
if m == nil {
|
||||
m = &moduleInfo{Module: e.Module}
|
||||
x.Modules[e.Module] = m
|
||||
}
|
||||
if e.Node != "" && !contains(m.On, e.Node) {
|
||||
m.On = append(m.On, e.Node)
|
||||
}
|
||||
m.Interchangeable = m.Interchangeable || e.Interchangeable
|
||||
key := e.Module + "." + e.Tool
|
||||
i, have := toolAt[key]
|
||||
if !have {
|
||||
i = len(l.Tools)
|
||||
toolAt[key] = i
|
||||
l.Tools = append(l.Tools, Tool{Module: e.Module, Name: e.Tool, Description: e.Description, Input: e.Schema})
|
||||
}
|
||||
if !contains(l.Tools[i].Subjects, e.Subject) {
|
||||
l.Tools[i].Subjects = append(l.Tools[i].Subjects, e.Subject)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// A tool's subjects as a call looks them up: the plain one any instance answers first, then each
|
||||
// machine's.
|
||||
for i := range l.Tools {
|
||||
t := &l.Tools[i]
|
||||
if t.Seat {
|
||||
continue
|
||||
}
|
||||
plain := "mesh.mod." + t.Module + ".tool." + t.Name
|
||||
sort.SliceStable(t.Subjects, func(a, b int) bool {
|
||||
if (t.Subjects[a] == plain) != (t.Subjects[b] == plain) {
|
||||
return t.Subjects[a] == plain
|
||||
}
|
||||
return t.Subjects[a] < t.Subjects[b]
|
||||
})
|
||||
}
|
||||
for _, t := range l.Tools {
|
||||
if !t.Seat {
|
||||
x.Modules[t.Module].Tools = append(x.Modules[t.Module].Tools, t)
|
||||
}
|
||||
}
|
||||
for _, m := range x.Modules {
|
||||
sort.Strings(m.On)
|
||||
}
|
||||
for _, st := range seats {
|
||||
sort.Slice(st.Holders, func(a, b int) bool { return st.Holders[a].Node < st.Holders[b].Node })
|
||||
x.Seats = append(x.Seats, *st)
|
||||
}
|
||||
sort.Slice(x.Seats, func(i, j int) bool { return x.Seats[i].Seat < x.Seats[j].Seat })
|
||||
sort.SliceStable(l.Tools, func(i, j int) bool {
|
||||
return l.Tools[i].Module+"."+l.Tools[i].Name < l.Tools[j].Module+"."+l.Tools[j].Name
|
||||
})
|
||||
|
||||
// What should have answered: every assignment of a module that declares tools. Silence is named;
|
||||
// a module with no tools is never a name here.
|
||||
@@ -410,6 +327,104 @@ func (s *Surface) indexAsked(fresh bool) (*index, error) {
|
||||
return x, nil
|
||||
}
|
||||
|
||||
// indexOfEndpoints is what the announced endpoints say: every seat with its verbs and holders, every
|
||||
// module with its tools and machines, the flat listing, and which module announced on which machine.
|
||||
func indexOfEndpoints(endpoints []announce.Endpoint) (*index, map[string]map[string]bool, map[string]bool) {
|
||||
l := &Listing{Tools: []Tool{}, NotAnswering: []string{}}
|
||||
x := &index{Modules: map[string]*moduleInfo{}, Listing: l, Recorded: map[string][]string{}}
|
||||
announced := map[string]map[string]bool{} // module → node → announced something
|
||||
seats := map[string]*seatInfo{}
|
||||
// <module>.<tool>, or seat:<seat>.<verb>, → index in l.Tools. **Apart**, because a seat and a module may
|
||||
// share a name — mesh-delivery is the delivery's seat and the module holding it (novox/hq ADR 0239), and
|
||||
// the module answers the seat's verbs with tools of the same names: in one namespace the module's tool
|
||||
// took the key first, and the seat was listed with no verb at all (novox/hq issue 287).
|
||||
toolAt := map[string]int{}
|
||||
machines := map[string]bool{}
|
||||
for _, e := range endpoints {
|
||||
if e.Node != "" {
|
||||
machines[e.Node] = true
|
||||
}
|
||||
if announced[e.Module] == nil {
|
||||
announced[e.Module] = map[string]bool{}
|
||||
}
|
||||
announced[e.Module][e.Node] = true
|
||||
switch e.Kind {
|
||||
case announce.KindSeat:
|
||||
st := seats[e.Seat]
|
||||
if st == nil {
|
||||
st = &seatInfo{Seat: e.Seat, Scope: e.Scope}
|
||||
seats[e.Seat] = st
|
||||
}
|
||||
if st.Scope != "node" && e.Scope == "node" {
|
||||
st.Scope = "node"
|
||||
}
|
||||
h := holder{Module: e.Module, Node: e.Node}
|
||||
if !containsHolder(st.Holders, h) {
|
||||
st.Holders = append(st.Holders, h)
|
||||
}
|
||||
key := "seat:" + e.Seat + "." + e.Tool
|
||||
if _, have := toolAt[key]; !have {
|
||||
toolAt[key] = len(l.Tools)
|
||||
t := Tool{Module: e.Seat, Name: e.Tool, Description: e.Description, Input: e.Schema, Seat: true, Scope: st.Scope}
|
||||
l.Tools = append(l.Tools, t)
|
||||
st.Verbs = append(st.Verbs, t)
|
||||
}
|
||||
case announce.KindTool:
|
||||
m := x.Modules[e.Module]
|
||||
if m == nil {
|
||||
m = &moduleInfo{Module: e.Module}
|
||||
x.Modules[e.Module] = m
|
||||
}
|
||||
if e.Node != "" && !contains(m.On, e.Node) {
|
||||
m.On = append(m.On, e.Node)
|
||||
}
|
||||
m.Interchangeable = m.Interchangeable || e.Interchangeable
|
||||
key := e.Module + "." + e.Tool
|
||||
i, have := toolAt[key]
|
||||
if !have {
|
||||
i = len(l.Tools)
|
||||
toolAt[key] = i
|
||||
l.Tools = append(l.Tools, Tool{Module: e.Module, Name: e.Tool, Description: e.Description, Input: e.Schema})
|
||||
}
|
||||
if !contains(l.Tools[i].Subjects, e.Subject) {
|
||||
l.Tools[i].Subjects = append(l.Tools[i].Subjects, e.Subject)
|
||||
}
|
||||
}
|
||||
}
|
||||
// A tool's subjects as a call looks them up: the plain one any instance answers first, then each
|
||||
// machine's.
|
||||
for i := range l.Tools {
|
||||
t := &l.Tools[i]
|
||||
if t.Seat {
|
||||
continue
|
||||
}
|
||||
plain := "mesh.mod." + t.Module + ".tool." + t.Name
|
||||
sort.SliceStable(t.Subjects, func(a, b int) bool {
|
||||
if (t.Subjects[a] == plain) != (t.Subjects[b] == plain) {
|
||||
return t.Subjects[a] == plain
|
||||
}
|
||||
return t.Subjects[a] < t.Subjects[b]
|
||||
})
|
||||
}
|
||||
for _, t := range l.Tools {
|
||||
if !t.Seat {
|
||||
x.Modules[t.Module].Tools = append(x.Modules[t.Module].Tools, t)
|
||||
}
|
||||
}
|
||||
for _, m := range x.Modules {
|
||||
sort.Strings(m.On)
|
||||
}
|
||||
for _, st := range seats {
|
||||
sort.Slice(st.Holders, func(a, b int) bool { return st.Holders[a].Node < st.Holders[b].Node })
|
||||
x.Seats = append(x.Seats, *st)
|
||||
}
|
||||
sort.Slice(x.Seats, func(i, j int) bool { return x.Seats[i].Seat < x.Seats[j].Seat })
|
||||
sort.SliceStable(l.Tools, func(i, j int) bool {
|
||||
return l.Tools[i].Module+"."+l.Tools[i].Name < l.Tools[j].Module+"."+l.Tools[j].Name
|
||||
})
|
||||
return x, announced, machines
|
||||
}
|
||||
|
||||
// target is what an address resolves to.
|
||||
type target struct {
|
||||
Address string
|
||||
@@ -433,9 +448,21 @@ func resolve(x *index, address string) (target, error) {
|
||||
}
|
||||
prefix, name := rest[:dot], rest[dot+1:]
|
||||
|
||||
if s := x.seat(prefix); s != nil {
|
||||
s := x.seat(prefix)
|
||||
if s != nil && x.Modules[prefix] != nil && findTool(x.Modules[prefix].Tools, name) != nil {
|
||||
// **A seat and a module of one name** (novox/hq issue 287): mesh-delivery is the delivery's seat and
|
||||
// the module holding it. The module's tool is meant when the seat has no such verb, or when a machine
|
||||
// is named for a seat held once for the mesh — `<node>/<module>.<tool>` is a module on one machine.
|
||||
if findTool(s.Verbs, name) == nil || (node != "" && s.Scope != "node") {
|
||||
s = nil
|
||||
}
|
||||
}
|
||||
if s != nil {
|
||||
verb := findTool(s.Verbs, name)
|
||||
if verb == nil {
|
||||
if len(s.Verbs) == 0 {
|
||||
return target{}, fmt.Errorf("the seat %s has no verb %s: its holder announced none", prefix, name)
|
||||
}
|
||||
return target{}, fmt.Errorf("the seat %s has no verb %s; it has %s", prefix, name, toolNames(s.Verbs))
|
||||
}
|
||||
if s.Scope == "node" {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package console
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-tools/node-tools/internal/announce"
|
||||
)
|
||||
|
||||
// **Issue 287**: mesh-delivery is the delivery's seat, held once for the mesh, and the module holding it,
|
||||
// which answers the seat's verbs with tools of the same names and has tools of its own. Discovery put the
|
||||
// module's tool and the seat's verb under one key, so the seat was listed with no verb; and every address
|
||||
// with the name resolved to the seat — `mesh-delivery.deliveries` and `novox/mesh-delivery.delivery_status`
|
||||
// alike were refused, the seat's verbs and the module's tools both out of reach.
|
||||
func TestASeatAndAModuleOfOneNameAreEachReached(t *testing.T) {
|
||||
var endpoints []announce.Endpoint
|
||||
for _, tool := range []string{"deliveries", "delivery_status"} {
|
||||
endpoints = append(endpoints, announce.Endpoint{Kind: announce.KindTool, Module: "mesh-delivery", Tool: tool,
|
||||
Node: "anchor", Subject: "mesh.mod.mesh-delivery.tool." + tool + ".anchor"})
|
||||
}
|
||||
for _, verb := range []string{"deliveries", "stalled"} {
|
||||
endpoints = append(endpoints, announce.Endpoint{Kind: announce.KindSeat, Module: "mesh-delivery", Seat: "mesh-delivery",
|
||||
Tool: verb, Scope: "mesh", Subject: "mesh.seat.mesh-delivery.tool." + verb})
|
||||
}
|
||||
x, _, _ := indexOfEndpoints(endpoints)
|
||||
|
||||
s := x.seat("mesh-delivery")
|
||||
if s == nil || len(s.Verbs) != 2 {
|
||||
t.Fatalf("the seat is listed with verbs %v, not its two", s)
|
||||
}
|
||||
if m := x.Modules["mesh-delivery"]; m == nil || len(m.Tools) != 2 {
|
||||
t.Fatalf("the module is listed with tools %v, not its two", m)
|
||||
}
|
||||
|
||||
for address, want := range map[string]struct {
|
||||
seat bool
|
||||
key string
|
||||
}{
|
||||
"mesh-delivery.deliveries": {true, "seat:mesh-delivery.deliveries"},
|
||||
"mesh-delivery.stalled": {true, "seat:mesh-delivery.stalled"},
|
||||
"anchor/mesh-delivery.delivery_status": {false, "mesh-delivery.delivery_status@anchor"},
|
||||
"anchor/mesh-delivery.deliveries": {false, "mesh-delivery.deliveries@anchor"},
|
||||
} {
|
||||
got, err := resolve(x, address)
|
||||
if err != nil {
|
||||
t.Errorf("%s: %v", address, err)
|
||||
continue
|
||||
}
|
||||
if got.Seat != want.seat || got.Key != want.key {
|
||||
t.Errorf("%s resolves to %+v, not the %s", address, got, want.key)
|
||||
}
|
||||
}
|
||||
// A module tool of the name without a machine: the module keeps state on its machine, so it says so.
|
||||
if _, err := resolve(x, "mesh-delivery.delivery_status"); err == nil || !strings.Contains(err.Error(), "<node>/") {
|
||||
t.Errorf("the module's tool without a machine: %v", err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user