The builder finds a build module as the indexer's seed does: the build seat whose acting rule `build` has an `on` matching the artifact's language or kind:image, and that seat's holder, read from the seat list's definitions and the claims of the catalog half's manifests. Task.Module, every lane key, the unpublished modules, the toolchain sources, the ends watched, the sayer and builder_stuck (through `<seat>.steps`) derive from it; the floor's sources are the holders of the seats git and mesh-catalog. buildrun.GoBuildModule, NodeBuildModule, LanguageOf, buildstep.BuildModuleOf use, build.ImageBuildModule and StuckBuildModules are gone from the builder (the checker keeps its own names in two small files until its part reads them from the list). Builds: design 47 G §7, §428, §704, §204, A-82 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 := FollowAssignments(context.Background(), bus, 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
|
|
}
|