A declaration newer in sequence than every other named four fewer modules than the assignments held, a machine applied it, and nothing the mesh kept said who sent it or from what view. The store now raises an assignment generation in the same transaction as every assignment change (a trigger, so a cascade counts too); a send reads it before composing, carries it to engines that said they read it, marks a gate's put-back, and is recorded with its sequence, sender, generation and modules. The would-send is stamped with what was last sent, so nothing reads behind for it; a refusal is kept on the send it refused and raised as S20 naming the sender; plan says what the machine was last told.
94 lines
4.1 KiB
Go
94 lines
4.1 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
)
|
|
|
|
// A declaration composed earlier is numbered lower than one composed later, whatever order the two
|
|
// are sent in (novox/hq issue 204). The number used to be taken at send time, after composing, so a
|
|
// declaration composed before an assignment changed and sent after a newer one carried the higher
|
|
// number — and the machine, which refuses a lower number, took the older content as the mesh's
|
|
// newest word. Taken before the composition reads anything, the order of numbers is the order of
|
|
// compositions, and the host's refusal does what it is for.
|
|
func TestADeclarationComposedEarlierIsNumberedLowerWhateverOrderItIsSent(t *testing.T) {
|
|
allot := numbered()
|
|
var composed []string
|
|
compose := func(stamp string) func(string) (sendable, error) {
|
|
return func(node string) (sendable, error) {
|
|
composed = append(composed, stamp)
|
|
return sendable{Resources: []map[string]any{{"id": node + "." + stamp}}}, nil
|
|
}
|
|
}
|
|
// Composed first — before an assignment changed — and sent last.
|
|
stale, _ := composeEach([]string{"anchor"}, allot, compose("before"))
|
|
// Composed after the change, sent first.
|
|
fresh, _ := composeEach([]string{"anchor"}, allot, compose("after"))
|
|
|
|
if stale[0].declared.Sequence != 1 || fresh[0].declared.Sequence != 2 {
|
|
t.Fatalf("the numbers do not follow the compositions: before=%d after=%d",
|
|
stale[0].declared.Sequence, fresh[0].declared.Sequence)
|
|
}
|
|
// Sent in the other order, the numbers do not change — so the machine that has applied the
|
|
// fresh one (2) refuses the stale one (1) when it arrives late.
|
|
if !(stale[0].declared.Sequence < fresh[0].declared.Sequence) {
|
|
t.Fatal("a declaration composed earlier must carry the lower number, however late it is sent")
|
|
}
|
|
if len(composed) != 2 || composed[0] != "before" {
|
|
t.Fatalf("compositions happened in an unexpected order: %v", composed)
|
|
}
|
|
}
|
|
|
|
// The number is taken before the first read of the composition, not after it: an allotter that
|
|
// fails leaves nothing composed for that machine, and the others are still composed.
|
|
func TestTheNumberIsTakenBeforeComposingAndItsFailureIsARefusal(t *testing.T) {
|
|
calls := 0
|
|
allot := func(node string) (order, error) {
|
|
if node == "anchor" {
|
|
return order{}, context.DeadlineExceeded
|
|
}
|
|
return order{sequence: 7}, nil
|
|
}
|
|
sending, refusals := composeEach([]string{"anchor", "laptop"}, allot, func(node string) (sendable, error) {
|
|
calls++
|
|
if node == "anchor" {
|
|
t.Fatal("anchor was composed although its number could not be taken")
|
|
}
|
|
return sendable{}, nil
|
|
})
|
|
if calls != 1 || len(sending) != 1 || sending[0].node != "laptop" || sending[0].declared.Sequence != 7 {
|
|
t.Fatalf("laptop should be composed with its number and anchor refused: %v / %v", sending, refusals)
|
|
}
|
|
if len(refusals) != 1 {
|
|
t.Fatalf("anchor's failed number should be a refusal naming it: %v", refusals)
|
|
}
|
|
}
|
|
|
|
// What was sent is written down even when the sender's context is already cancelled (issue 204): a
|
|
// controller replaced mid-send had told the machine and never recorded it, so status read "applied,
|
|
// current" over a machine that had just been sent something else.
|
|
func TestASendIsRecordedEvenWhenTheSenderIsBeingCancelled(t *testing.T) {
|
|
inv := inventory.ForTest(t)
|
|
ctx, cancel := context.WithCancel(t.Context())
|
|
if _, err := inv.AddNode(ctx, "anchor"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cancel() // the sender is going away: its context is cancelled between the send and the record
|
|
body := []byte(`{"declaration":1,"resources":[]}`)
|
|
digest, err := recordSent(ctx, inv, "anchor", body, nil, 0, sentRecord{sender: "a test"})
|
|
if err != nil {
|
|
// NodeByName on the cancelled context may itself refuse; the record must still be possible
|
|
// through the detached context, so look the node up again on a live one.
|
|
t.Fatalf("recording a send after cancellation failed: %v", err)
|
|
}
|
|
outstanding, err := inv.Outstanding(t.Context(), "anchor")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if outstanding != digest || digest != digestOf(body) {
|
|
t.Fatalf("the send was not recorded: outstanding %q, sent %q", outstanding, digest)
|
|
}
|
|
}
|