consumes seat:mesh-module-assigner.views.assignments, reads seat:mesh-module-assigner.assignments; the holder is asked of the seat list (seatlist.HolderOf), the first view is the holder's first-view tool (A-131), the assigner's callers list gains the builder; FollowAssignments takes the bucket. Builds: design 47 G A-119, A-116, A-131
120 lines
4.2 KiB
Go
120 lines
4.2 KiB
Go
package buildrun
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"log"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/nats-io/nats.go/jetstream"
|
|
|
|
"git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
|
|
"git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
|
meshwants "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
|
|
)
|
|
|
|
// placing is a value of the state `assignments` placing modules on node.
|
|
func placing(t *testing.T, node string, modules ...string) (string, []byte) {
|
|
t.Helper()
|
|
p := meshmoduleassigner.Assignments{Node: node}
|
|
for _, m := range modules {
|
|
p.Modules = append(p.Modules, meshmoduleassigner.PlacedModule{Module: m})
|
|
}
|
|
values, err := meshmoduleassignerclient.AssignmentsValues(p)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
key, _ := meshmoduleassignerclient.NodeKey(node)
|
|
return key, values[key]
|
|
}
|
|
|
|
// The placements the builder counts nodes by (ADR 0624 §1): a settled assignments names the nodes its modules stand on, a
|
|
// revision of the assignment half the same; they hold no toolchain (design 47 G §546: a toolchain is forgotten on the
|
|
// node-engine seat's stopped, never on a placement).
|
|
func TestPlacedNodes(t *testing.T) {
|
|
old := time.Date(2026, 10, 23, 6, 29, 18, 0, time.UTC)
|
|
three := []string{"g14", "novox", "shanks"}
|
|
|
|
t.Run("the assignments names the counted nodes once it is settled", func(t *testing.T) {
|
|
gate := NewPlacedNodes(func(string, ...any) {})
|
|
for _, node := range three {
|
|
k, v := placing(t, node, testNode)
|
|
if gate.Assignments(k, v, false, old) {
|
|
t.Fatal("an assignments before it is settled changed the counted nodes")
|
|
}
|
|
}
|
|
gate.Settled()
|
|
if got := strings.Join(gate.Nodes(), ","); got != "g14,novox,shanks" {
|
|
t.Fatalf("nodes = %q", got)
|
|
}
|
|
k, v := placing(t, "ace", testGo)
|
|
if !gate.Assignments(k, v, false, old) || len(gate.Nodes()) != 4 {
|
|
t.Fatalf("ace placed: nodes %v", gate.Nodes())
|
|
}
|
|
if !gate.Assignments(k, nil, true, old.Add(time.Hour)) || len(gate.Nodes()) != 3 {
|
|
t.Fatalf("ace's assignments deleted: nodes %v", gate.Nodes())
|
|
}
|
|
})
|
|
|
|
t.Run("the assignment half: the first revision settles, a later one moves the nodes", func(t *testing.T) {
|
|
gate := NewPlacedNodes(func(string, ...any) {})
|
|
revision := func(n int64, nodes ...string) meshwants.AssignmentRevision {
|
|
at := old.Add(time.Duration(n) * time.Hour)
|
|
r := meshwants.AssignmentRevision{Revision: n, Kind: "trunk", At: &at}
|
|
for _, node := range nodes {
|
|
r.Entries = append(r.Entries, meshwants.NodeEntryRef{Node: node, Module: testNode, Digest: "d-" + node})
|
|
}
|
|
return r
|
|
}
|
|
if !gate.Revision(revision(7, three...)) || len(gate.Nodes()) != 3 {
|
|
t.Fatalf("the first revision: nodes %v", gate.Nodes())
|
|
}
|
|
if gate.Revision(revision(7, three...)) {
|
|
t.Fatal("the same revision again changed the counted nodes")
|
|
}
|
|
if !gate.Revision(revision(9, "novox", "shanks")) || len(gate.Nodes()) != 2 {
|
|
t.Fatalf("g14 taken off: nodes %v", gate.Nodes())
|
|
}
|
|
})
|
|
|
|
t.Run("assignments the bus refuses is said, and the part goes on", func(t *testing.T) {
|
|
var logs bytes.Buffer
|
|
logger := log.New(&logs, "", 0)
|
|
refused := errors.New("nats: permissions violation for publish to $JS.API.CONSUMER.CREATE.KV_mesh-delivery-assigner_assignments")
|
|
gate := NewPlacedNodes(logger.Printf)
|
|
for _, bus := range []refusingBus{{err: refused}, {refuseOpen: true, err: refused}} {
|
|
if _, err := FollowAssignments(context.Background(), bus, AssignmentsBucket, gate, func(context.Context) {}, logger.Printf); err != nil {
|
|
t.Errorf("a refused assignments stopped the start: %v", err)
|
|
}
|
|
}
|
|
if !strings.Contains(logs.String(), "cannot be watched") || !strings.Contains(logs.String(), "cannot be read") {
|
|
t.Errorf("the refusals were not said: %q", logs.String())
|
|
}
|
|
})
|
|
}
|
|
|
|
// refusingBus is a bus that refuses: every open (refuseOpen), or every watch of a bucket it opens, with err.
|
|
type refusingBus struct {
|
|
refuseOpen bool
|
|
err error
|
|
}
|
|
|
|
func (r refusingBus) KeyValue(context.Context, string) (jetstream.KeyValue, error) {
|
|
if r.refuseOpen {
|
|
return nil, r.err
|
|
}
|
|
return refusedWatch{err: r.err}, nil
|
|
}
|
|
|
|
type refusedWatch struct {
|
|
jetstream.KeyValue
|
|
err error
|
|
}
|
|
|
|
func (r refusedWatch) WatchAll(context.Context, ...jetstream.WatchOpt) (jetstream.KeyWatcher, error) {
|
|
return nil, r.err
|
|
}
|