A wait for a build is the value of <node>.<module> with state pending and a pending object (id p-<unix nanoseconds>, a string), so every reader that skips a value with a state skips it, and the controller's own lists skip it through Stands. The transitions are compare-and-set on the key's revision (claim, release, settle, repoint, mark); assigning over a wait carries it into the value that stands, applied when it was waiting; a plan pass never overwrites it; one open wait per machine and module, and a key that holds the module refuses a new one. The table is no longer read or written, ForgetEndedPending is gone (no key is removed), and a wait of a machine the store dropped is none. Callers change type only; their conditions are the next issue.
167 lines
7.4 KiB
Go
167 lines
7.4 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
"github.com/novox/mesh-controller/internal/conditions"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// failedBuild settles the merge's build of modules/sensors as failed.
|
|
|
|
func conditionOpen(t *testing.T, p inventory.PendingAssignment) bool {
|
|
t.Helper()
|
|
_, found, err := conditionsFrom.Get(t.Context(), pendingObservation(p).Key())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return found
|
|
}
|
|
|
|
func rowOf(t *testing.T, open *stores, id string) inventory.PendingAssignment {
|
|
t.Helper()
|
|
rows, err := open.inventory.Pending(t.Context(), time.Unix(0, 0))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, r := range rows {
|
|
if r.ID == id {
|
|
return r
|
|
}
|
|
}
|
|
t.Fatalf("pending assignment %s is not on record", id)
|
|
return inventory.PendingAssignment{}
|
|
}
|
|
|
|
// novox/hq issue 325: an assignment of a module the catalogue does not hold says which case it is in — a
|
|
// build in flight (kept pending), known and not built (said, with build), or unknown (refused, with the
|
|
// closest names) — and a pending assignment is made when its build registers the module, or ends with why.
|
|
|
|
// aCatalogueMesh is aMesh with one module held from the catalogue, so a merge of it is acted on.
|
|
|
|
// mergeAdding is the catalogue's merge adding one module's directory, acted on as the bus hands it over.
|
|
|
|
// outcome is a build's outcome as the build seat announces it: registered as module, or failed.
|
|
func outcome(id, dir, commit, module, failed string) link.BuildResult {
|
|
r := link.BuildResult{ID: id, Repository: "novox/mesh-catalog", Path: dir, Ref: "main", Commit: commit,
|
|
On: "anchor", Failed: failed, Source: &link.SourceOnSeat{Repository: "novox/mesh-catalog", Seat: "git"}}
|
|
if failed == "" {
|
|
r.Module = module
|
|
r.Manifest, _ = json.Marshal(catalogue.Manifest{Module: module, Version: "1"})
|
|
}
|
|
return r
|
|
}
|
|
|
|
func assignedTo(t *testing.T, open *stores, node string) []string {
|
|
t.Helper()
|
|
got, err := open.inventory.Assigned(t.Context(), node)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return got
|
|
}
|
|
|
|
func pendingOf(t *testing.T, open *stores) []inventory.PendingAssignment {
|
|
t.Helper()
|
|
got, err := open.inventory.Pending(t.Context(), time.Now().Add(-time.Hour))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return got
|
|
}
|
|
|
|
// A build in flight: the merge asked for it, the request is kept as the merge's, the assignment is kept
|
|
// pending and said so with the build, status reads it without settling anything, and the build's outcome
|
|
// makes it — saying that a push sends it, and nothing more.
|
|
|
|
// **A read settles nothing** (review of #150, point 6): status — and so the board, the summary and the
|
|
// probe, which compose from the same reading — leaves a pending assignment whose module is registered as it
|
|
// is; the controller's tick makes it.
|
|
|
|
// The build of a pending assignment fails: it expires with the build's words and nothing is assigned; the
|
|
// tick raises it as a condition once; it then reads as known and not built; and `unassign` takes it back,
|
|
// after which the tick clears the condition. Status is not called well while the condition is open, and is
|
|
// again once it clears — no fixed day of "not well" (review point 6).
|
|
|
|
// The condition also clears when the module is later assigned to the machine.
|
|
|
|
// Known and not built, asked with build: the build is asked from where the last one was, the request kept as
|
|
// assign's, and the assignment kept pending on the new build.
|
|
|
|
// **build "true" with a pending assignment already waiting** (review point 5): the waiting one is made to
|
|
// wait for the new build, and no build is asked that nothing waits on.
|
|
|
|
// **A failed ask never reads as in flight** (review point 2): a merge whose ask could not be made keeps the
|
|
// request as not asked, and assign says the merge could not ask; a request whose asker could not hand it over
|
|
// or stopped waiting reads the same, unless its outcome was heard.
|
|
|
|
// A plan's tier keeps its requests as the plan's (review point 3).
|
|
|
|
// **A build heard as built and not registered yet is in flight** (review point 4): the outcome is recorded
|
|
// before the module is registered; for that moment assign keeps the assignment pending. Past the grace it is
|
|
// recorded and not registered, and the tick ends the pending assignment with that.
|
|
|
|
// Unknown: refused as no module of that name, claiming only what was looked at, with the closest names; build
|
|
// asks for nothing.
|
|
|
|
// Several modules in one act, one of them not registered: nothing is assigned and nothing kept pending, and
|
|
// each missing one is said.
|
|
|
|
// unassign withdraws a waiting pending assignment; the build goes on and registers the module assigned
|
|
// nowhere: a withdrawn row is never claimed.
|
|
|
|
// **A claim is never overridden** (review point 1): a pending assignment being made is claimed
|
|
// (waiting → applying); an unassign meanwhile is refused and says so, and leaves the claim; making a row
|
|
// already withdrawn makes nothing. A claim left by a controller that stopped is settled by the tick from what
|
|
// the mesh holds: back to waiting when the module is not assigned, applied when it is.
|
|
|
|
// A build that says nothing within the bound: the pending assignment expires at the next tick, saying so,
|
|
// rather than waiting for ever; and a module registered by a process that kept no pending assignment is
|
|
// assigned at the next tick.
|
|
|
|
// The seat's assign passes build on; unassign takes none.
|
|
func TestTheSeatsAssignPassesBuild(t *testing.T) {
|
|
argv, err := argvFor("assign", map[string]any{"node": "g14", "module": "sensors", "build": "true"})
|
|
if err != nil || !slices.Equal(argv, []string{"assign", "g14", "sensors", "--build"}) {
|
|
t.Fatalf("assign with build: %v %v", argv, err)
|
|
}
|
|
if _, err := argvFor("unassign", map[string]any{"node": "g14", "module": "sensors", "build": "true"}); err == nil {
|
|
t.Fatal("unassign took build")
|
|
}
|
|
}
|
|
|
|
// The condition's words are plain.
|
|
func TestAnAssignmentNotMadeIsSaidPlainly(t *testing.T) {
|
|
o := pendingObservation(inventory.PendingAssignment{Node: "g14", Module: "sensors",
|
|
Note: "the pending assignment of sensors to g14 expired: build b-1 of modules/sensors failed: boom"})
|
|
w := plainWordings[kindPendingEnded](o)
|
|
if why, ok := conditions.PlainWords(w, "g14"); !ok {
|
|
t.Fatalf("not plain: %s %+v", why, w)
|
|
}
|
|
if w.Headline != "sensors was not put on g14" {
|
|
t.Fatalf("headline %q", w.Headline)
|
|
}
|
|
}
|
|
|
|
// **One row's condition is never another's** (second review, bug A). The sequence: the first pending
|
|
// assignment's build fails and the tick raises it; the person assigns again with build "true", a second
|
|
// pending assignment; its build fails too before the next tick. That tick raises the second, and must not
|
|
// clear it by judging the first answered by the second: each row has its own condition, and a newer row that
|
|
// itself ended unmade answers nothing.
|
|
|
|
// **A raised row outlives the pruning until its condition clears** (second review, bug B); and a machine's
|
|
// removal, which takes its rows with it, clears their conditions at the next tick.
|
|
|
|
// **A claim left by a controller that stopped** (second review): the tick settles it from what the mesh
|
|
// holds — back to waiting when the module is not assigned there, applied when it is — and leaves a fresh
|
|
// claim alone.
|
|
|
|
// **A waited build whose asker stopped waiting is asked, outcome unknown** (second review): it may still run,
|
|
// so it reads as in flight until its outcome or its bound, never as not asked; a build never handed over
|
|
// is not asked.
|