buildrun gains AssignerHolder, AssignmentsOf and SleepFor (moved from the builder's program, which uses them), so the builder and the checker follow the holder's first-view tool, views family and state alike; the checker's manifest consumes the seat's views assignments and reads its assignments state, its composition reads the picture once followed. The assigner's callers list for the first view gains the checker. Builds: design 47 G A-82, A-119, A-126, A-131 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
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 := FollowAssignmentsIn(context.Background(), bus, "acme-assigner_assignments", 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
|
|
}
|