153 lines
6.5 KiB
Go
153 lines
6.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
)
|
|
|
|
// novox/hq ADR 0344, issue 641: the operator's one Approve on a settings proposal is the act that makes it take
|
|
// effect. The warrant sets the layer and opens a settings walk, which sends it to the machines it reaches one at a
|
|
// time, judging the module on each; the ask says so before the Approve, and nothing names a push.
|
|
|
|
// The order: every machine running the module, or the one the layer names, the control node last.
|
|
func TestASettingsWalkGoesToTheMachinesTheLayerReachesTheControlNodeLast(t *testing.T) {
|
|
running := []string{"shanks", "novox", "g14", "ace"}
|
|
for _, c := range []struct {
|
|
node string
|
|
want []string
|
|
}{
|
|
{"", []string{"ace", "g14", "shanks", "novox"}},
|
|
{"g14", []string{"g14"}},
|
|
{"novox", []string{"novox"}},
|
|
{"elsewhere", nil},
|
|
} {
|
|
if got := settingsWalkOrder(c.node, running, []string{"novox"}); !reflect.DeepEqual(got, c.want) {
|
|
t.Errorf("a layer on %q goes to %v, want %v", c.node, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A settings walk sends a machine only when no other walk judges there.
|
|
func TestASettingsWalkWaitsWhileAnotherWalkJudgesOnTheMachine(t *testing.T) {
|
|
judging := &inventory.PlanGate{Machines: []string{"g14"}}
|
|
passed := &inventory.PlanGate{Machines: []string{"g14"}, Verdict: inventory.GatePassed}
|
|
plans := []inventory.Plan{
|
|
{ID: "settings-1", State: inventory.PlanRolling, Release: &inventory.PlanRelease{Gate: judging}},
|
|
{ID: "release-2", State: inventory.PlanRolling, Release: &inventory.PlanRelease{Gate: passed}},
|
|
{ID: "plan-3", State: inventory.PlanRolling, Modules: map[string]*inventory.PlanModule{"app": {Gate: judging}}},
|
|
{ID: "plan-4", State: inventory.PlanDone, Modules: map[string]*inventory.PlanModule{"app": {Gate: judging}}},
|
|
}
|
|
if id := judgingIn(plans, "settings-1", "g14"); id != "plan-3" {
|
|
t.Errorf("on g14 %q judges, want plan-3 (itself, a passed gate and an ended walk are not judging)", id)
|
|
}
|
|
if id := judgingIn(plans[1:2], "x", "g14"); id != "" {
|
|
t.Errorf("a passed gate judges: %q", id)
|
|
}
|
|
if id := judgingIn(plans, "x", "shanks"); id != "" {
|
|
t.Errorf("on shanks %q judges", id)
|
|
}
|
|
}
|
|
|
|
// The ask says, before the Approve, that the layer is sent at once and judged, and that its end is told as every
|
|
// walk's is; neither the ask nor the proposer's answer names a push any more.
|
|
func TestTheAskSaysAnApprovedLayerIsSentAtOnce(t *testing.T) {
|
|
for _, c := range []struct {
|
|
in proposeInput
|
|
where string
|
|
had bool
|
|
}{
|
|
{proposeInput{module: "mounts", node: "shanks", values: mountsSources}, "shanks", false},
|
|
{proposeInput{module: "notes", values: map[string]any{"x": "1"}}, "every machine running notes", true},
|
|
{proposeInput{module: "notes", node: "laptop", clear: true}, "laptop", true},
|
|
} {
|
|
r := newProposerRig(t)
|
|
if c.had {
|
|
r.before, r.had = map[string]any{"x": "0"}, true
|
|
}
|
|
words, err := r.pr.propose(context.Background(), c.in)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
q := r.askSent(t)
|
|
if err := q.Check(r.now); err != nil {
|
|
t.Fatalf("the ask is refused: %v", err)
|
|
}
|
|
// At a glance (ADR 0345): the summary says it is sent at once by a walk, one machine at a time; the rest of
|
|
// what Approve does is the Details', which the router notes after the answers.
|
|
if !strings.Contains(q.Explanation, "sent at once by a walk to "+c.where+", one machine at a time, each judged") ||
|
|
!strings.Contains(q.Details, "and sent at once to "+c.where+", one machine at a time, each judged, "+
|
|
"every build there kept; its end is told as every walk's is") {
|
|
t.Errorf("the ask does not say the layer is sent at once to %s:\n%s", c.where, q.Explanation)
|
|
}
|
|
approve := q.Options[0]
|
|
if approve.Label != "Approve" || !strings.Contains(approve.Does, "sent to "+c.where+" at once") {
|
|
t.Errorf("Approve does %q", approve.Does)
|
|
}
|
|
if !strings.Contains(words, "a walk sends it to "+c.where+" at once") {
|
|
t.Errorf("the proposer's answer:\n%s", words)
|
|
}
|
|
for _, said := range []string{q.Explanation, q.Whole, approve.Does, words} {
|
|
if strings.Contains(said, "push") {
|
|
t.Errorf("a push is still named, as if a second act were needed:\n%s", said)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ---- on the real stores -----------------------------------------------------------------------------
|
|
|
|
// The warrant sets the layer and opens the walk that sends it, naming the machines it reaches; a layer for a
|
|
// machine that does not run the module opens none and says so.
|
|
|
|
// walkMesh is a mesh where `app` runs c1 on anchor and laptop and nothing waits for a gate: every send applied at
|
|
// once, recorded, and judged from the reports, with a machine named broken failing what it was sent.
|
|
type walkMesh struct {
|
|
open *stores
|
|
sent [][]string
|
|
broken map[string]bool
|
|
// kept says, per send, whether it kept every build of its machine; moves is the data a machine's send would move.
|
|
kept []bool
|
|
moves map[string][]dataMove
|
|
heard map[string]bool
|
|
}
|
|
|
|
func (w *walkMesh) walk(t *testing.T) inventory.Plan {
|
|
t.Helper()
|
|
plans, err := w.open.inventory.RecentPlans(t.Context(), 10)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, p := range plans {
|
|
if p.Release != nil && p.Release.Settings != nil {
|
|
return p
|
|
}
|
|
}
|
|
t.Fatal("no settings walk")
|
|
return inventory.Plan{}
|
|
}
|
|
|
|
// The walk sends the first machine alone and judges the module there before it sends the next, and ends
|
|
// delivered on both; nothing waited for a gate, so each send carried the layer alone.
|
|
|
|
// A machine that fails what it was sent stops the walk there: the next is not sent, the layer stays, and the
|
|
// failure holds back no walk of waiting builds.
|
|
|
|
// Each send of a settings walk keeps every build of its machine: the layer goes, and a build held for a person or
|
|
// waiting for a gate stays where it is (ADR 0344 §3).
|
|
|
|
// A machine whose send would move a running module's data stops the walk there, failed and said with the push that
|
|
// sends it on a person's word (ADR 0217 §3); one not heard from is passed over and named; and a walk that reached
|
|
// no machine is not said delivered.
|
|
|
|
// The gate keeps the module's build its send kept, so a crossing send — a merge's walk sending the machine a new
|
|
// build of the module while the settings gate judges there — supersedes it, and its report is never read as the
|
|
// settings send's (issue 641 review, finding 1).
|
|
|
|
// A send refusal that cannot clear by itself — what the machine was last sent is not known, or a build it runs is
|
|
// gone — fails the walk with why, rather than keeping it open and holding the walk of waiting builds; and a
|
|
// failure names the machines passed over as away (issue 641 review, findings 2 and 3).
|