108 lines
6.5 KiB
Go
108 lines
6.5 KiB
Go
package assign
|
|
|
|
import (
|
|
"encoding/json"
|
|
"git.novox.be/novox/mesh-sdk/go/keys"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
mmac "git.novox.be/novox/mesh-contracts/go/clients/meshmoduleassignerclient"
|
|
mma "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner"
|
|
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
|
|
|
plc "mesh-delivery-assigner/internal/assignment"
|
|
)
|
|
|
|
// ADR 0567 §2, §5, §7, §8: what the seat's holder decides of a proposal: every refusal named at once, a valid one laid
|
|
// with the base revision of each key it changes, the writes at those revisions, the controller's calls that keep its
|
|
// assignments in step, the authorising ask bound to the changes, and the export read as the controller answers it.
|
|
func TestTheAssignmentsSeatDecides(t *testing.T) {
|
|
anchor := mma.Assignments{V: keys.Version, Node: "anchor", Decided: mma.NodeDecided{Hub: true, Address: "10.0.0.1"},
|
|
Modules: []mma.PlacedModule{{Module: "store", Ports: map[string]int64{"5432": 5432}}, {Module: "web"}}}
|
|
laptop := mma.Assignments{V: keys.Version, Node: "laptop", Decided: mma.NodeDecided{Address: "10.0.0.2"}, Modules: []mma.PlacedModule{{Module: "shell"}}}
|
|
in := func(changes ...mma.ProposalChange) ProposeIn {
|
|
return ProposeIn{ID: "p-one", By: "operator@laptop", Why: "move web", At: time.Unix(1, 0), Changes: changes,
|
|
Current: map[string]mma.Assignments{"anchor": anchor, "laptop": laptop}, Revisions: map[string]uint64{"anchor": 4, "laptop": 9},
|
|
Held: func(m string) bool { return m != "ghost" },
|
|
Recorded: func(n string) bool { return n == "anchor" || n == "laptop" }}
|
|
}
|
|
move := []mma.ProposalChange{{Op: mmac.OpTakeOff, Node: "anchor", Module: "web"}, {Op: mmac.OpPlace, Node: "laptop", Module: "web"}}
|
|
p, laid, findings := Propose(in(move...))
|
|
if len(findings) != 0 || p.State != mmac.ProposalAnswering || p.Base["anchor"] != 4 || p.Base["laptop"] != 9 ||
|
|
!slices.Equal(mmac.Assigned(*laid["laptop"]), []string{"shell", "web"}) || !slices.Equal(mmac.Assigned(*laid["anchor"]), []string{"store"}) {
|
|
t.Fatalf("a move: %+v, %+v, %v", p, laid, findings)
|
|
}
|
|
if _, err := plc.EncodeProposal(p); err != nil {
|
|
t.Fatalf("the proposal cannot be written: %v", err)
|
|
}
|
|
for name, c := range map[string]struct {
|
|
in ProposeIn
|
|
want string
|
|
}{
|
|
"a module not held": {in(mma.ProposalChange{Op: mmac.OpPlace, Node: "laptop", Module: "ghost"}), "the indexer holds no module ghost"},
|
|
"a node with no record": {in(mma.ProposalChange{Op: mmac.OpPlace, Node: "stranger", Module: "web"}), "stranger is a node with no record"},
|
|
"nothing changes": {in(mma.ProposalChange{Op: mmac.OpPlace, Node: "anchor", Module: "web"}), "leave every assignments as it is"},
|
|
"no why": {func() ProposeIn { i := in(move...); i.Why = " "; return i }(), "says why"},
|
|
"no caller": {func() ProposeIn { i := in(move...); i.By = ""; return i }(), "names no caller"},
|
|
"a seed by hand": {in(mma.ProposalChange{Op: mmac.OpSeed, Node: "laptop", Whole: &laptop}), "gone with adopt"},
|
|
// 1126: the controller ended by its unassignment; placed again it would be a second writer beside the parts that
|
|
// act without its lease.
|
|
"the controller placed again": {in(mma.ProposalChange{Op: mmac.OpPlace, Node: "laptop", Module: "mesh-controller"}), "ended by its unassignment"},
|
|
"the controller seeded whole": {in(mma.ProposalChange{Op: mmac.OpSeed, Node: "anchor", Whole: &mma.Assignments{V: keys.Version, Node: "anchor",
|
|
Decided: anchor.Decided, Modules: []mma.PlacedModule{{Module: "mesh-controller"}}}}), "two writers (issue 1126)"},
|
|
"a pin of nobody placed": {in(mma.ProposalChange{Op: mmac.OpPin, Node: "laptop", Provision: "db", Provider: &mma.Provider{Node: "laptop", Module: "store"}}), "not placed there"},
|
|
} {
|
|
if _, _, f := Propose(c.in); !slices.ContainsFunc(f, func(s string) bool { return strings.Contains(s, c.want) }) {
|
|
t.Errorf("%s: findings %v, want one naming %q", name, f, c.want)
|
|
}
|
|
}
|
|
|
|
// Every kind of change is taken: the assignments is the one source, and the controller's verbs limit nothing (ADR 0567 §8).
|
|
for _, c := range []mma.ProposalChange{
|
|
{Op: mmac.OpConfig, Node: "anchor", Module: "web", Values: map[string]any{"puid": 1}},
|
|
{Op: mmac.OpPlace, Node: "laptop", Module: "web", Ports: map[string]int64{"80": 8080}, Bindings: map[string]mma.Provider{"db": {Node: "anchor", Module: "store"}}},
|
|
{Op: mmac.OpConfig, Node: "anchor", Module: "web", Layer: map[string]any{"places": []any{}}},
|
|
{Op: mmac.OpConfig, Node: "anchor", Module: "web", MeshWide: map[string]any{"accesses": []any{}}},
|
|
{Op: mmac.OpDecide, Node: "laptop", Field: "site", Value: json.RawMessage(`"attic"`)},
|
|
{Op: mmac.OpHold, Node: "anchor", Module: "store", Claim: "node-shell", Scope: "node"},
|
|
} {
|
|
if _, _, f := Propose(in(c)); len(f) != 0 {
|
|
t.Errorf("%s is refused: %v", c.Op, f)
|
|
}
|
|
}
|
|
|
|
// A key moved while the warrant wrote: what is left to write is the other nodes' changes, to be laid and asked again.
|
|
if rest, ok := Rest(p, map[string]*mma.Assignments{"anchor": laid["anchor"]}); !ok || len(rest.Changes) != 1 || rest.Changes[0].Node != "laptop" ||
|
|
len(rest.Base) != 1 || rest.State != mmac.ProposalAnswering || rest.Must != nil {
|
|
t.Errorf("the rest = %+v, %v", rest, ok)
|
|
}
|
|
if _, ok := Rest(p, laid); ok {
|
|
t.Error("a proposal written whole leaves something to write")
|
|
}
|
|
|
|
// The seat says it waits while one of its states is not bound, naming it, and clears once all are.
|
|
for _, unbound := range [][]string{{plc.PulsesBucket}, nil} {
|
|
c := SeatWaitsCondition(unbound)
|
|
if _, err := occ.EncodeCondition(c); err != nil || (len(unbound) > 0) != (c.State == occ.StateRaised) ||
|
|
(len(unbound) > 0 && !strings.Contains(c.Explanation, "the seat waits for its states: "+plc.PulsesBucket)) {
|
|
t.Errorf("waiting for %v: %+v, %v", unbound, c, err)
|
|
}
|
|
}
|
|
|
|
writes, err := Writes(p, laid)
|
|
if err != nil || len(writes) != 2 || writes[0].Key != "anchor.node" || writes[0].Base != 4 || writes[1].Base != 9 {
|
|
t.Fatalf("writes = %+v, %v", writes, err)
|
|
}
|
|
a, err := AssignmentsAsk("assignments.p-one", p, "move web (proposed by operator@laptop)", 7, time.Unix(100, 0))
|
|
if err != nil || occ.Check(mustAsk(t, a), time.Unix(100, 0)) != nil || mustAsk(t, a).Options[0].Level != occ.Approve || occ.Performs(mustAsk(t, a).Options[0], AssignmentsAct(p)) != nil {
|
|
t.Fatalf("the ask = %+v, %v", a, err)
|
|
}
|
|
q := p
|
|
q.Changes = append(slices.Clone(p.Changes), mma.ProposalChange{Op: mmac.OpUnpin, Node: "laptop", Provision: "x"})
|
|
if occ.Performs(mustAsk(t, a).Options[0], AssignmentsAct(q)) == nil {
|
|
t.Error("the ask's yes binds changes it was not asked about")
|
|
}
|
|
}
|