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
261 lines
12 KiB
Go
261 lines
12 KiB
Go
package buildrun
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
|
|
meshwants "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants"
|
|
"git.novox.be/novox/mesh-delivery/sdk/buildstep"
|
|
)
|
|
|
|
func gateReport(t *testing.T, node, version string, at time.Time) (string, []byte) {
|
|
t.Helper()
|
|
key, _ := buildstep.ToolchainKey(node, "typescript")
|
|
value, err := buildstep.EncodeToolchain(buildstep.Toolchain{Language: "typescript", Version: version, Recipe: "f59571cf4be75adf", At: at})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return key, value
|
|
}
|
|
|
|
// placing is a value of the state `assignments` placing modules on node.
|
|
|
|
func agreedOf(set *ToolchainSet) (string, string) {
|
|
tc, none := buildstep.AgreedToolchain(set.Entries("typescript"))
|
|
return tc.Version, none
|
|
}
|
|
|
|
// Issue 1940, the gate over the catalogue's assignments (issue 774, ADR 0549 §3): a build node's report counts only while
|
|
// the assignments places its build module on its node, and only once written since; before the assignments is settled, and
|
|
// while no node's assignments is held, every report counts. The live cases: mesh-build-node taken off ace left
|
|
// ace.typescript at v26.12.0 against v26.12.1 on the three others; put on ace again, its report of 2026-10-23 06:29 came
|
|
// back before the module ran there.
|
|
func TestToolchainGate(t *testing.T) {
|
|
old := time.Date(2026, 10, 23, 6, 29, 18, 0, time.UTC) // ace's report from its earlier run
|
|
placed := time.Date(2026, 10, 25, 19, 56, 45, 0, time.UTC) // mesh-build-node placed on ace again
|
|
three := []string{"g14", "novox", "shanks"}
|
|
start := func(t *testing.T, seeded bool) (*ToolchainGate, *ToolchainSet, *[]string) {
|
|
set := &ToolchainSet{Seats: testSeats()}
|
|
said := &[]string{}
|
|
gate := NewToolchainGate([]string{testGo, testNode}, set.Put, func(f string, a ...any) { *said = append(*said, fmt.Sprintf(f, a...)) })
|
|
for _, node := range three {
|
|
if seeded {
|
|
key, value := placing(t, node, testNode)
|
|
gate.Assignments(key, value, false, old.Add(-time.Hour))
|
|
}
|
|
key, value := gateReport(t, node, "v26.12.1", placed.Add(time.Minute))
|
|
gate.Toolchain(testNode, key, value, false)
|
|
}
|
|
return gate, set, said
|
|
}
|
|
|
|
t.Run("not placed on ace: its report is no builder's", func(t *testing.T) {
|
|
gate, set, said := start(t, true)
|
|
key, value := gateReport(t, "ace", "v26.12.0", old)
|
|
gate.Toolchain(testNode, key, value, false)
|
|
if a := buildstep.ToolchainAgreement(set.Entries("typescript")); len(a.SittingOut) != 1 || a.SittingOut[0].Node != "ace" { // ADR 0584: the majority is agreed, ace's report counts as the one that sits out
|
|
t.Fatalf("before the assignments is settled every report counts; got %+v", a)
|
|
}
|
|
k, v := placing(t, "ace", testGo) // another build module on ace: no matter
|
|
gate.Assignments(k, v, false, old)
|
|
if !gate.Settled() {
|
|
t.Fatal("settling left ace's report out, so what was passed on changed")
|
|
}
|
|
if v, none := agreedOf(set); v != "v26.12.1" || none != "" {
|
|
t.Fatalf("the placed nodes agree on v26.12.1; got %q, %q", v, none)
|
|
}
|
|
if len(*said) != 1 || !strings.Contains((*said)[0], "does not place mesh-build-node on ace") {
|
|
t.Fatalf("ace's report is said left out once; said %q", *said)
|
|
}
|
|
if gate.Toolchain(testNode, key, value, false) {
|
|
t.Fatal("a report of a node not placed changed what was passed on")
|
|
}
|
|
// Placed on ace with a report written since: it counts, and the builders really disagree.
|
|
k, v = placing(t, "ace", testGo, testNode)
|
|
gate.Assignments(k, v, false, old.Add(-time.Minute))
|
|
if a := buildstep.ToolchainAgreement(set.Entries("typescript")); a.Toolchain.Version != "v26.12.1" || len(a.SittingOut) != 1 || a.SittingOut[0].Version != "v26.12.0" {
|
|
t.Fatalf("builders that run different versions: the majority is agreed and ace sits out; got %+v", a)
|
|
}
|
|
// Taken off ace again (its key deleted): its report goes again.
|
|
if !gate.Assignments(k, nil, true, placed) {
|
|
t.Fatal("taking ace's assignments away passed nothing on")
|
|
}
|
|
if v, none := agreedOf(set); v != "v26.12.1" || none != "" {
|
|
t.Fatalf("got %q, %q", v, none)
|
|
}
|
|
// A report deleted from the bucket goes whether its node is placed or not.
|
|
key, _ = gateReport(t, "g14", "v26.12.1", placed)
|
|
if !gate.Toolchain(testNode, key, nil, true) || len(set.Entries("typescript")) != 2 {
|
|
t.Fatalf("deleting g14's report: %d entries left, want novox and shanks", len(set.Entries("typescript")))
|
|
}
|
|
})
|
|
|
|
t.Run("build modules given later pass their reports on when the assignments moves", func(t *testing.T) {
|
|
set := &ToolchainSet{Seats: testSeats()}
|
|
gate := NewToolchainGate(nil, set.Put, func(string, ...any) {})
|
|
gate.Settled()
|
|
k, v := placing(t, "novox", testGo)
|
|
gate.Assignments(k, v, false, old)
|
|
key, value := gateReport(t, "ace", "v26.12.1", placed.Add(time.Minute))
|
|
gate.Toolchain(testNode, key, value, false) // ace is not placed: left out
|
|
k, v = placing(t, "ace", testNode)
|
|
if gate.Assignments(k, v, false, placed) || len(set.Entries("typescript")) != 0 {
|
|
t.Fatalf("a module the gate was not given passed on at the assignments: %v", set.Entries("typescript"))
|
|
}
|
|
if !gate.UseModules([]string{testNode}) || len(set.Entries("typescript")) != 1 {
|
|
t.Fatalf("given mesh-build-node, ace's report placed since is passed on; entries %v", set.Entries("typescript"))
|
|
}
|
|
})
|
|
|
|
for _, c := range []struct {
|
|
name string
|
|
at time.Time // when ace wrote its report
|
|
counts bool
|
|
}{
|
|
{"a report left before the assignments", old, false},
|
|
{"a report written since the assignments", placed.Add(30 * time.Second), true},
|
|
} {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
gate, set, said := start(t, true)
|
|
k, v := placing(t, "ace") // ace's key, first heard placing nothing
|
|
gate.Assignments(k, v, false, old.Add(-time.Hour))
|
|
gate.Settled()
|
|
k, v = placing(t, "ace", testNode)
|
|
if gate.Assignments(k, v, false, placed) {
|
|
t.Fatal("assignments with no report passed something on")
|
|
}
|
|
key, value := gateReport(t, "ace", "v26.12.0", c.at)
|
|
if got := gate.Toolchain(testNode, key, value, false); got != c.counts {
|
|
t.Fatalf("ace's report passed on: %v, want %v", got, c.counts)
|
|
}
|
|
// ace's assignments written again with another module beside: its standing does not move, nothing is said again.
|
|
k, v = placing(t, "ace", testGo, testNode)
|
|
if gate.Assignments(k, v, false, placed.Add(time.Minute)) {
|
|
t.Fatal("the same assignments of mesh-build-node changed what was passed on")
|
|
}
|
|
if c.counts {
|
|
return
|
|
}
|
|
if v, none := agreedOf(set); v != "v26.12.1" || none != "" {
|
|
t.Fatalf("ace's left report is no builder's; got %q, %q", v, none)
|
|
}
|
|
if len(*said) != 1 || !strings.Contains((*said)[0], "before mesh-build-node was placed on ace at 2026-10-25T19:56:45Z") {
|
|
t.Fatalf("said %q, want once why ace's report is left out", *said)
|
|
}
|
|
key, value = gateReport(t, "ace", "v26.12.1", placed.Add(time.Minute))
|
|
if !gate.Toolchain(testNode, key, value, false) || len(set.Entries("typescript")) != 4 {
|
|
t.Fatal("ace's new report was not passed on")
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("no assignments held: every report counts", func(t *testing.T) {
|
|
gate, set, _ := start(t, false)
|
|
key, value := gateReport(t, "ace", "v26.12.0", old)
|
|
gate.Toolchain(testNode, key, value, false)
|
|
gate.Settled()
|
|
if n := len(set.Entries("typescript")); n != 4 {
|
|
t.Fatalf("before the first assignments file every report counts; got %d", n)
|
|
}
|
|
k, v := placing(t, "g14", testNode)
|
|
if !gate.Assignments(k, v, false, old.Add(-time.Hour)) || len(set.Entries("typescript")) != 1 {
|
|
t.Fatalf("the first assignments leaves every node it does not place out; got %d", len(set.Entries("typescript")))
|
|
}
|
|
if !gate.Assignments(k, nil, true, placed) || len(set.Entries("typescript")) != 4 {
|
|
t.Fatal("the last assignments gone: every report counts again")
|
|
}
|
|
})
|
|
|
|
// Design 47 *Each part acts on its own entries*: fed by the assignment half's revisions, the gate settles at the first,
|
|
// leaves out a node no revision places the build module on, and counts a node placed in a later revision only by a
|
|
// report written since that revision's time.
|
|
t.Run("fed by the assignment half", func(t *testing.T) {
|
|
set := &ToolchainSet{Seats: testSeats()}
|
|
said := &[]string{}
|
|
gate := NewToolchainGate([]string{testGo, testNode}, set.Put, func(f string, a ...any) { *said = append(*said, fmt.Sprintf(f, a...)) })
|
|
for _, node := range append([]string{"ace"}, three...) {
|
|
key, value := gateReport(t, node, "v26.12.1", placed.Add(time.Minute))
|
|
if node == "ace" {
|
|
key, value = gateReport(t, node, "v26.12.0", old)
|
|
}
|
|
gate.Toolchain(testNode, key, value, false)
|
|
}
|
|
revision := func(n int64, at time.Time, nodes ...string) meshwants.AssignmentRevision {
|
|
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, old.Add(-time.Hour), three...)) || len(set.Entries("typescript")) != 3 {
|
|
t.Fatalf("the first revision settles the gate and leaves ace out; %d entries", len(set.Entries("typescript")))
|
|
}
|
|
if gate.Revision(revision(7, old.Add(-time.Hour), three...)) {
|
|
t.Fatal("the same revision again changed what was passed on")
|
|
}
|
|
if gate.Revision(revision(8, placed, "ace", "g14", "novox", "shanks")); len(set.Entries("typescript")) != 3 {
|
|
t.Fatal("ace's report from before revision 8 placed it counts")
|
|
}
|
|
if len(*said) < 2 || !strings.Contains((*said)[len(*said)-1], "before mesh-build-node was placed on ace at 2026-10-25T19:56:45Z") {
|
|
t.Fatalf("said %q, want why ace's old report is left out", *said)
|
|
}
|
|
key, value := gateReport(t, "ace", "v26.12.1", placed.Add(time.Minute))
|
|
if !gate.Toolchain(testNode, key, value, false) || len(set.Entries("typescript")) != 4 {
|
|
t.Fatal("ace's report written since revision 8 is not passed on")
|
|
}
|
|
if !gate.Revision(revision(9, placed.Add(time.Hour), "ace", "novox", "shanks")) || len(set.Entries("typescript")) != 3 || len(gate.Nodes()) != 3 {
|
|
t.Fatalf("g14 taken off in revision 9: %d entries, nodes %v", len(set.Entries("typescript")), gate.Nodes())
|
|
}
|
|
})
|
|
|
|
t.Run("the history: a module in the first revision heard of a key stands since before it", func(t *testing.T) {
|
|
p := meshmoduleassignerclient.NewPlaced()
|
|
// ace's node key is the first revision heard of it: its module stands since before.
|
|
k, v := placing(t, "ace", testNode)
|
|
p.Put(k, v, false, old)
|
|
if _, changed, _ := p.Put("ace."+testNode, []byte("{}"), false, placed); len(changed) != 0 {
|
|
t.Errorf("a module key says which modules stand: %v", changed)
|
|
}
|
|
k2, v2 := placing(t, "g14")
|
|
p.Put(k2, v2, false, old)
|
|
k2, v2 = placing(t, "g14", testNode)
|
|
if _, changed, _ := p.Put(k2, v2, false, placed); strings.Join(changed, ",") != testNode {
|
|
t.Errorf("a module placed says changed %v", changed)
|
|
}
|
|
if _, _, err := p.Put("novox.node", []byte("not json"), false, old); err == nil {
|
|
t.Error("a value that does not read was not said")
|
|
}
|
|
if s, ok := p.Since("ace", testNode); !ok || !s.IsZero() {
|
|
t.Errorf("ace's module stands since %v, %v; want the zero time", s, ok)
|
|
}
|
|
if s, ok := p.Since("g14", testNode); !ok || !s.Equal(placed) {
|
|
t.Errorf("g14's module stands since %v, %v; want the revision that placed it", s, ok)
|
|
}
|
|
if got := p.PlacedNodes(testNode); strings.Join(got, ",") != "ace,g14" || len(p.ByNode()) != 2 || !p.Seeded() {
|
|
t.Errorf("PlacedNodes = %v, ByNode = %v", got, p.ByNode())
|
|
}
|
|
})
|
|
|
|
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, _, _ := start(t, false)
|
|
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())
|
|
}
|
|
})
|
|
}
|