Seals are reused and compared by slot (resource, field), never across resources; one rule says which resolved config values are laid; a seat path that steps back is owned by the seat it steps back for, whatever the seat order; a folded module of an old-form build is composed from what runs of it; the operator export's file is carried as it runs; a differing resource names its differing JSON paths, found before its bodies are cut, and an answer is fitted to the node's key.
383 lines
20 KiB
Go
383 lines
20 KiB
Go
package delivery
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
)
|
|
|
|
// ADR 0549 §1, §2: the placement key and file, the value's refusals, its reading as the composition reads a node, what
|
|
// a change says, and the catalogue's check of every file.
|
|
func TestPlacement(t *testing.T) {
|
|
t.Run("key and file", func(t *testing.T) {
|
|
key, err := PlacementKey("g14")
|
|
if err != nil || key != "placement.g14" {
|
|
t.Fatalf("PlacementKey = %q, %v", key, err)
|
|
}
|
|
if n, err := ParsePlacementKey(key); err != nil || n != "g14" {
|
|
t.Fatalf("ParsePlacementKey = %q, %v", n, err)
|
|
}
|
|
for _, bad := range []string{"want.g14.x", "placement.", "placement.a.b"} {
|
|
if _, err := ParsePlacementKey(bad); err == nil {
|
|
t.Errorf("ParsePlacementKey(%q) took it", bad)
|
|
}
|
|
}
|
|
if _, err := PlacementKey("a.b"); err == nil {
|
|
t.Error("PlacementKey took a dotted node")
|
|
}
|
|
if PlacementBucket != "mesh-delivery-indexer_placement" || PlacementFile("ace") != "placement/ace.json" {
|
|
t.Fatalf("bucket %q, file %q", PlacementBucket, PlacementFile("ace"))
|
|
}
|
|
for p, want := range map[string]string{"placement/ace.json": "ace", "./placement/ace.json": "ace",
|
|
"placement/x/ace.json": "", "modules/ace.json": "", "placement/ace.yaml": ""} {
|
|
if n, ok := PlacementFileNode(p); n != want || ok != (want != "") {
|
|
t.Errorf("PlacementFileNode(%q) = %q, %v", p, n, ok)
|
|
}
|
|
}
|
|
})
|
|
|
|
g14 := Placement{Node: "g14", Revision: "abc",
|
|
Decided: NodeDecided{Endpoint: "g14.example:51820", Site: "home", Address: "10.77.0.2", Account: "op", Taken: []string{"zsh"}},
|
|
Modules: []PlacedModule{
|
|
{Module: "zsh"},
|
|
{Module: "app", Ports: map[string]int{"8080": 18080}, Bindings: map[string]Provider{"postgres": {Node: "novox", Module: "postgres"}}},
|
|
},
|
|
Pins: map[string]Provider{"shell": {Node: "g14", Module: "zsh"}},
|
|
Holds: []Holding{{Claim: "node-shell", Scope: "node", Module: "zsh"}},
|
|
}
|
|
novox := Placement{Node: "novox", Decided: NodeDecided{Hub: true, Address: "10.77.0.1"}, Modules: []PlacedModule{{Module: "postgres", Ports: map[string]int{"5432": 5432}}},
|
|
Holds: []Holding{{Claim: "mesh-store", Scope: "mesh", Module: "postgres"}}}
|
|
|
|
t.Run("value", func(t *testing.T) {
|
|
raw, err := EncodePlacement(g14)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
back, err := DecodePlacement(raw)
|
|
if err != nil || back.V != Version || !SamePlacement(back, g14) {
|
|
t.Fatalf("round trip: %+v, %v", back, err)
|
|
}
|
|
moved := g14
|
|
moved.Revision = "def"
|
|
if !SamePlacement(g14, moved) {
|
|
t.Error("a new revision alone counts as a change")
|
|
}
|
|
if _, err := DecodePlacement([]byte(`{"v":9,"node":"g14","modules":[]}`)); !errors.Is(err, ErrUnknownVersion) {
|
|
t.Errorf("version 9: %v", err)
|
|
}
|
|
for name, bad := range map[string]string{
|
|
"module twice": `{"v":1,"node":"g14","modules":[{"module":"a"},{"module":"a"}]}`,
|
|
"port not number": `{"v":1,"node":"g14","modules":[{"module":"a","ports":{"http":80}}]}`,
|
|
"host port zero": `{"v":1,"node":"g14","modules":[{"module":"a","ports":{"80":0}}]}`,
|
|
"binding no node": `{"v":1,"node":"g14","modules":[{"module":"a","bindings":{"pg":{"module":"pg"}}}]}`,
|
|
"pin no module": `{"v":1,"node":"g14","modules":[],"pins":{"pg":{"node":"x"}}}`,
|
|
"hold not placed": `{"v":1,"node":"g14","modules":[],"holds":[{"claim":"s","module":"a"}]}`,
|
|
"node not a name": `{"v":1,"node":"g.14","modules":[]}`,
|
|
"value not shaped": `{"v":1,"node":"g14","modules":{}}`,
|
|
} {
|
|
if _, err := DecodePlacement([]byte(bad)); err == nil {
|
|
t.Errorf("%s: taken", name)
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("as the composition reads it", func(t *testing.T) {
|
|
if got := g14.Assigned(); !reflect.DeepEqual(got, []string{"zsh", "app"}) {
|
|
t.Errorf("Assigned = %v, want the file's order", got)
|
|
}
|
|
if got := g14.ChosenPins(); !reflect.DeepEqual(got, map[string]catalogue.Chosen{"shell": {Node: "g14", Module: "zsh"}}) {
|
|
t.Errorf("ChosenPins = %v", got)
|
|
}
|
|
if got := g14.Bound(); !reflect.DeepEqual(got, map[string]map[string]catalogue.Chosen{"app": {"postgres": {Node: "novox", Module: "postgres"}}}) {
|
|
t.Errorf("Bound = %v", got)
|
|
}
|
|
if got := g14.HostPorts(); !reflect.DeepEqual(got, map[string]map[int]int{"app": {8080: 18080}}) {
|
|
t.Errorf("HostPorts = %v", got)
|
|
}
|
|
if got := g14.Held(); !reflect.DeepEqual(got, []catalogue.Held{{Claim: "node-shell", Scope: "node", Node: "g14", Module: "zsh"}}) {
|
|
t.Errorf("Held = %v", got)
|
|
}
|
|
layered := PlacedModule{Module: "app", HasMesh: true, Layer: map[string]any{"expose": map[string]any{"80": "mesh"}}}
|
|
want := []catalogue.Layer{
|
|
{From: catalogue.MeshWideLayer, Values: map[string]any{}},
|
|
{From: "g14", Values: map[string]any{"mode": "x"}, Placement: map[string]any{"expose": map[string]any{"80": "mesh"}}},
|
|
}
|
|
if got := layered.Layers("g14", map[string]any{"mode": "x"}); !reflect.DeepEqual(got, want) {
|
|
t.Errorf("Layers = %#v, want %#v", got, want)
|
|
}
|
|
if got := (PlacedModule{Module: "a"}).Layers("g14", map[string]any{"k": 1}); len(got) != 1 || got[0].From != "g14" {
|
|
t.Errorf("values alone make the node's layer: %#v", got)
|
|
}
|
|
if got := (PlacedModule{Module: "a", HasLayer: true}).Layers("g14", nil); len(got) != 1 || got[0].Values == nil {
|
|
t.Errorf("an empty layer of the node's own is kept, with empty values: %#v", got)
|
|
}
|
|
if got := (PlacedModule{Module: "a"}).Layers("g14", nil); got != nil {
|
|
t.Errorf("no layer and no value is no layer: %#v", got)
|
|
}
|
|
empty := Placement{Node: "x"}
|
|
if empty.ChosenPins() != nil || empty.Bound() != nil || empty.HostPorts() != nil || len(empty.Held()) != 0 {
|
|
t.Error("an empty placement reads as something")
|
|
}
|
|
})
|
|
|
|
t.Run("what a change says", func(t *testing.T) {
|
|
after := g14
|
|
after.Revision = "def"
|
|
after.Modules = []PlacedModule{{Module: "app", Ports: map[string]int{"8080": 18081}}, {Module: "new"}}
|
|
got := ChangeOf(g14, after)
|
|
want := PlacementChange{Node: "g14", Revision: "def", Added: []string{"new"}, Removed: []string{"zsh"}, Changed: []string{"app"}}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Errorf("ChangeOf = %+v, want %+v", got, want)
|
|
}
|
|
pinned := g14
|
|
pinned.Pins = nil
|
|
if got := ChangeOf(g14, pinned); !reflect.DeepEqual(got.Changed, []string{"app", "zsh"}) {
|
|
t.Errorf("a pin change changes every module on the node: %+v", got)
|
|
}
|
|
if got := ChangeOf(Placement{}, novox); !reflect.DeepEqual(got.Added, []string{"postgres"}) || got.Node != "novox" {
|
|
t.Errorf("a new node: %+v", got)
|
|
}
|
|
})
|
|
|
|
t.Run("the catalogue's check", func(t *testing.T) {
|
|
held := func(m string) bool { return m != "ghost" }
|
|
enrolled := func(n string) bool { return n != "stranger" }
|
|
if r := CheckPlacements(map[string]Placement{"g14": g14, "novox": novox}, held, enrolled); len(r) != 0 {
|
|
t.Fatalf("a sound placement is refused: %v", r)
|
|
}
|
|
// A replicated mesh seat (ADR 0223) is held on two nodes; a seat that is not replicated is refused there.
|
|
for seat, refused := range map[string]bool{"mesh-dns-resolver": false, "mesh-broker": true} {
|
|
a := Placement{Node: "ace", Modules: []PlacedModule{{Module: "dns"}}, Holds: []Holding{{Claim: seat, Scope: "mesh", Module: "dns"}}}
|
|
b := a
|
|
b.Node = "novox"
|
|
r := CheckPlacements(map[string]Placement{"ace": a, "novox": b}, held, enrolled)
|
|
if (len(r) != 0) != refused {
|
|
t.Errorf("%s on two nodes: refusals %v, want refused %v", seat, r, refused)
|
|
}
|
|
}
|
|
clash := g14
|
|
clash.Modules = append([]PlacedModule{{Module: "ghost", Ports: map[string]int{"80": 18080}}}, g14.Modules...)
|
|
clash.Modules[2] = PlacedModule{Module: "app", Ports: map[string]int{"8080": 18080}, Bindings: map[string]Provider{"postgres": {Node: "ace", Module: "postgres"}}}
|
|
clash.Pins = map[string]Provider{"shell": {Node: "g14", Module: "bash"}}
|
|
twice := novox
|
|
twice.Node = "stranger"
|
|
twice.Decided.Address = "10.77.0.2"
|
|
r := CheckPlacements(map[string]Placement{"g14": clash, "novox": novox, "stranger": twice}, held, enrolled)
|
|
for _, want := range []string{"holds no module ghost", "ghost and app are both published on host port 18080",
|
|
"bound to ace/postgres", "pin of shell names g14/bash", "stranger is not an enrolled node",
|
|
"mesh-store, held once for the mesh, is held on novox and stranger",
|
|
"g14 and stranger both have the address 10.77.0.2", "novox, stranger are each placed as it"} {
|
|
found := false
|
|
for _, line := range r {
|
|
found = found || strings.Contains(line, want)
|
|
}
|
|
if !found {
|
|
t.Errorf("no refusal says %q; got %v", want, r)
|
|
}
|
|
}
|
|
})
|
|
|
|
// Issue 1126: a catalogue pull request's placement, one candidate key per node, and each node's answer in its have.
|
|
t.Run("candidates", func(t *testing.T) {
|
|
key, err := PlacementCandidateKey("feat/1.2", "g14")
|
|
if err != nil || key != "feat/1=2e2.g14" {
|
|
t.Fatalf("PlacementCandidateKey = %q, %v", key, err)
|
|
}
|
|
if g, n, err := ParsePlacementCandidateKey(key); err != nil || g != "feat/1.2" || n != "g14" {
|
|
t.Fatalf("ParsePlacementCandidateKey = %q, %q, %v", g, n, err)
|
|
}
|
|
for _, bad := range []string{"g14", ".g14", "feat.g.14", "feat.", "fe=t.g14"} {
|
|
if _, _, err := ParsePlacementCandidateKey(bad); err == nil {
|
|
t.Errorf("ParsePlacementCandidateKey(%q) took it", bad)
|
|
}
|
|
}
|
|
if PlacementCandidatesBucket != "mesh-delivery-indexer_placement-candidates" {
|
|
t.Errorf("bucket %q", PlacementCandidatesBucket)
|
|
}
|
|
pull := Head{Repository: CatalogueRepository, Number: 7, Head: "h1"}
|
|
for name, c := range map[string]PlacementCandidate{
|
|
"placed": {Group: "feat/x", Pull: pull, Placement: &g14},
|
|
"removed": {Group: "feat/x", Pull: pull},
|
|
} {
|
|
raw, err := EncodePlacementCandidate(c)
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", name, err)
|
|
}
|
|
back, err := DecodePlacementCandidate(raw)
|
|
if err != nil || back.Group != c.Group || back.Pull != pull || (back.Placement == nil) != (c.Placement == nil) ||
|
|
(c.Placement != nil && !SamePlacement(*back.Placement, g14)) {
|
|
t.Errorf("%s: round trip %+v, %v", name, back, err)
|
|
}
|
|
}
|
|
for name, c := range map[string]PlacementCandidate{
|
|
"no group": {Pull: pull},
|
|
"no head": {Group: "g", Pull: Head{Repository: CatalogueRepository, Number: 7}},
|
|
"bad place": {Group: "g", Pull: pull, Placement: &Placement{Node: "a.b"}},
|
|
"no repo form": {Group: "g", Pull: Head{Repository: "mesh-catalog", Number: 7, Head: "h"}},
|
|
} {
|
|
if _, err := EncodePlacementCandidate(c); err == nil {
|
|
t.Errorf("%s: taken", name)
|
|
}
|
|
}
|
|
answers := map[string]CandidateAnswer{
|
|
"same": {Head: "h1", Revision: 3, Verdict: CandidateSame, Same: []string{"app", "node"}},
|
|
"differs": {Head: "h1", Revision: 3, Verdict: CandidateDiffers, Differs: []ModuleDiffers{{Module: "app", Resource: "file:/etc/app", Why: "the resource differs"}}},
|
|
"refused": {Head: "h1", Revision: 3, Verdict: CandidateRefused, Refused: "unbound postgres"},
|
|
"added": {Head: "h1", Verdict: CandidateDiffers, Same: []string{"node"}, Added: []ModuleAdded{{Module: "app", Unissued: []string{"app/token"}}}, Removed: []string{"old"}},
|
|
"a value": {Head: "h1", Verdict: CandidateRefused, Same: []string{"node"}, Refusals: []ModuleRefused{{Module: "db", Why: "stands in for db/password"}}},
|
|
// A head that deletes the node's placement file: the node returns to the controller's compositions (issue 1126,
|
|
// the seed's revert, mesh-catalog #526).
|
|
"returns": {Head: "h1", Verdict: CandidateSame, Returns: ReturnsToController, Same: []string{"app", "node"}},
|
|
"returns, the controller's differs": {Head: "h1", Verdict: CandidateDiffers, Returns: ReturnsToController, Same: []string{"node"},
|
|
Differs: []ModuleDiffers{{Module: "app", Resource: "file:/etc/app", Why: "the resource differs"}}, Removed: []string{"old"}},
|
|
}
|
|
n := HaveNode{Composed: "sha256:" + strings.Repeat("a", 64), Sealed: []string{"box"}, Candidates: answers,
|
|
Composes: ComposesOwn, ComposesWhy: "the catalogue places it (placement.n1)"}
|
|
raw, err := EncodeHaveNode(n)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
back, err := DecodeHaveNode(raw)
|
|
if err != nil || !SameHaveNode(back, HaveNode{V: Version, Composed: n.Composed, Sealed: n.Sealed, Candidates: answers,
|
|
Composes: ComposesOwn, ComposesWhy: n.ComposesWhy}) {
|
|
t.Fatalf("a node's answers changed on the way: %+v, %v", back, err)
|
|
}
|
|
if SameHaveNode(back, HaveNode{V: Version, Composed: n.Composed, Sealed: n.Sealed, Composes: ComposesOwn, ComposesWhy: n.ComposesWhy}) {
|
|
t.Error("a node's have without its answers counts as the same")
|
|
}
|
|
if SameHaveNode(back, HaveNode{V: Version, Composed: n.Composed, Sealed: n.Sealed, Candidates: answers,
|
|
Composes: ComposesController, ComposesWhy: n.ComposesWhy}) {
|
|
t.Error("a node's have composing otherwise counts as the same")
|
|
}
|
|
if _, err := EncodeHaveNode(HaveNode{Composes: "both"}); err == nil {
|
|
t.Error("a node composing neither its own nor the controller's was taken")
|
|
}
|
|
// CompareComposed: the bus's user list is left out on both sides; anything else that differs is named.
|
|
file := func(id, content string) json.RawMessage {
|
|
return json.RawMessage(`{"id":"` + id + `","type":"file","module":"nats","path":"/etc/` + id + `","mode":"0600","content":"` + content + `"}`)
|
|
}
|
|
fp := "src2:" + strings.Repeat("a", 64)
|
|
comp := func(rs ...json.RawMessage) Composed {
|
|
return Composed{Node: "novox", Module: "nats", FP: fp, Resources: rs}
|
|
}
|
|
for name, tc := range map[string]struct {
|
|
composed, running Composed
|
|
resource, why string
|
|
}{
|
|
"user list alone": {comp(file("conf", "a"), file("nats.bus-users", "stand-in")), comp(file("conf", "a"), file("nats.bus-users", "real")), "", ""},
|
|
"a resource": {comp(file("conf", "a")), comp(file("conf", "b")), "conf", "the resource differs"},
|
|
"one side": {comp(file("conf", "a"), file("more", "x")), comp(file("conf", "a")), "more", "composed, and not in what runs"},
|
|
"order": {comp(file("conf", "a"), file("more", "x")), comp(file("more", "x"), file("conf", "a")), "", "the same resources in another order"},
|
|
} {
|
|
c := CompareComposed(map[string]Composed{"novox.nats." + fp: tc.composed}, []Composed{tc.running})
|
|
if tc.why == "" {
|
|
if len(c.Differs) != 0 || len(c.Same) != 1 {
|
|
t.Errorf("%s: %+v", name, c)
|
|
}
|
|
} else if len(c.Differs) != 1 || c.Differs[0].Resource != tc.resource || c.Differs[0].Why != tc.why ||
|
|
c.Differs[0].Composed == "" || c.Differs[0].Running == "" || c.Differs[0].Composed == c.Differs[0].Running {
|
|
t.Errorf("%s: differs %+v, want %s: %s with both digests", name, c.Differs, tc.resource, tc.why)
|
|
}
|
|
}
|
|
// Issue 1126, the shadow shows its work: every differing resource with both bodies, each value a secret could be
|
|
// in masked (never a secret in an answer), saying whether the two sides hold the same value.
|
|
sealedFile := func(id, sealed, mode string) json.RawMessage {
|
|
return json.RawMessage(`{"id":"` + id + `","type":"file","module":"nats","path":"/etc/` + id + `","mode":"` + mode + `","sealed":"` + sealed + `"}`)
|
|
}
|
|
// Issue 1126, C1: a sealed value is judged by the value it opens to at its own slot: b sealed again (box-2 and
|
|
// box-3 open to one value) is no difference; d holds at its slot a box of another value, though that value is
|
|
// the one b holds, and differs at its path `sealed`.
|
|
sameBox := func(a, b string) (same, known bool) {
|
|
return (a == "box-2" && b == "box-3") || a == b, true
|
|
}
|
|
shown := CompareComposedWith(map[string]Composed{"k": comp(sealedFile("a", "box-1", "0600"), sealedFile("b", "box-2", "0600"),
|
|
file("c", "x"), sealedFile("d", "box-2", "0600"))},
|
|
[]Composed{comp(sealedFile("a", "box-1", "0644"), sealedFile("b", "box-3", "0600"), file("c", "y"), sealedFile("d", "box-4", "0600"))},
|
|
sameBox)
|
|
if len(shown.Differs) != 1 || len(shown.Differs[0].Bodies) != 3 {
|
|
t.Fatalf("every differing resource is not shown, or b sealed again is shown: %+v", shown.Differs)
|
|
}
|
|
if c := CompareComposedWith(map[string]Composed{"k": comp(sealedFile("b", "box-2", "0600"))},
|
|
[]Composed{comp(sealedFile("b", "box-3", "0600"))}, sameBox); len(c.Same) != 1 {
|
|
t.Errorf("a value sealed again at its own slot is not the same: %+v", c)
|
|
}
|
|
for _, row := range []struct {
|
|
id, has, hasNot string
|
|
paths []string
|
|
}{
|
|
{"a", `"mode":"0600"`, "box-1", []string{"mode"}},
|
|
{"c", `"content":"x"`, "", []string{"content"}},
|
|
{"d", "<masked: another value>", "box-2", []string{"sealed"}},
|
|
} {
|
|
var body ResourceBodies
|
|
for _, b := range shown.Differs[0].Bodies {
|
|
if b.ID == row.id {
|
|
body = b
|
|
}
|
|
}
|
|
both := body.Composed + body.Running
|
|
if !strings.Contains(both, row.has) || (row.hasNot != "" && strings.Contains(both, row.hasNot)) ||
|
|
!slices.Equal(body.Paths, row.paths) {
|
|
t.Errorf("%s shown as %+v, want paths %v", row.id, body, row.paths)
|
|
}
|
|
}
|
|
// Issue 1126, T1: the paths are found on the whole bodies, before they are cut; JSON in a string is walked into, a
|
|
// key a secret could be under is named and never shown.
|
|
long := strings.Repeat("x", MaxBody)
|
|
conf := func(link, password string) json.RawMessage {
|
|
inner, _ := json.Marshal(map[string]any{"pad": long, "links": []string{"a", link}, "password": password})
|
|
outer, _ := json.Marshal(map[string]any{"id": "conf", "type": "file", "module": "nats", "path": "/etc/conf",
|
|
"mode": "0600", "content": string(inner)})
|
|
return outer
|
|
}
|
|
far := CompareComposedWith(map[string]Composed{"k": comp(conf("b", "hunter2"))}, []Composed{comp(conf("c", "hunter3"))}, nil)
|
|
if len(far.Differs) != 1 || len(far.Differs[0].Bodies) != 1 ||
|
|
!slices.Equal(far.Differs[0].Bodies[0].Paths, []string{"content.links.1", "content.password"}) ||
|
|
!strings.Contains(far.Differs[0].Bodies[0].Composed, "cut at") || strings.Contains(fmt.Sprint(far), "hunter") {
|
|
t.Errorf("a difference past the cut: %+v", far.Differs)
|
|
}
|
|
// And an answer too large for the node's key is fitted: the texts go first, the verdict and the paths stay.
|
|
big := CandidateAnswer{Head: "h", Verdict: CandidateDiffers, Differs: far.Differs}
|
|
if fitted := FitAnswer(big, 4<<10); len(fitted.Differs[0].Bodies[0].Composed) != 0 || len(fitted.Differs[0].Bodies[0].Paths) != 2 ||
|
|
fitted.Verdict != CandidateDiffers || !strings.Contains(fitted.Note, "cut") || big.Differs[0].Bodies[0].Composed == "" {
|
|
t.Errorf("fitted as %+v", fitted)
|
|
}
|
|
if c, r := MaskedPair(json.RawMessage(`{"env":{"DB_PASSWORD":"hunter2","PORT":"80"},"list":[{"token":"t1"}]}`),
|
|
json.RawMessage(`{"env":{"DB_PASSWORD":"hunter3","PORT":"81"},"list":[{"token":"t1"}]}`), nil); strings.Contains(c+r, "hunter") ||
|
|
strings.Contains(c+r, `"t1"`) || !strings.Contains(r, `"PORT":"81"`) || !strings.Contains(c, "<masked: differs>") ||
|
|
!strings.Contains(c, "<masked: the same on both sides>") {
|
|
t.Errorf("masked as %s and %s", c, r)
|
|
}
|
|
if c := CompareComposed(nil, []Composed{{Node: "novox", Resources: []json.RawMessage{}}}); len(c.Removed) != 1 || c.Removed[0] != "node" {
|
|
t.Errorf("the node's own composition run and not composed is removed: %+v", c)
|
|
}
|
|
if c := CompareComposed(map[string]Composed{"k": comp(file("conf", "a"))}, nil); len(c.Added) != 1 || c.Added[0].Module != "nats" || c.Added[0].Composed == "" {
|
|
t.Errorf("a composition not run is added: %+v", c)
|
|
}
|
|
for name, a := range map[string]CandidateAnswer{
|
|
"no head": {Verdict: CandidateSame},
|
|
"same with a diff": {Head: "h", Verdict: CandidateSame, Differs: answers["differs"].Differs},
|
|
"differs with none": {Head: "h", Verdict: CandidateDiffers},
|
|
"refused no reason": {Head: "h", Verdict: CandidateRefused},
|
|
"unknown verdict": {Head: "h", Verdict: "maybe"},
|
|
"diff with no module": {Head: "h", Verdict: CandidateDiffers, Differs: []ModuleDiffers{{Why: "x"}}},
|
|
"same with an added": {Head: "h", Verdict: CandidateSame, Added: []ModuleAdded{{Module: "a"}}},
|
|
"differs refusing": {Head: "h", Verdict: CandidateDiffers, Removed: []string{"a"}, Refusals: []ModuleRefused{{Module: "b", Why: "x"}}},
|
|
"refused and modules": {Head: "h", Verdict: CandidateRefused, Refused: "x", Same: []string{"a"}},
|
|
"returns refusing": {Head: "h", Verdict: CandidateRefused, Returns: ReturnsToController, Refusals: []ModuleRefused{{Module: "b", Why: "x"}}},
|
|
"returns refused": {Head: "h", Verdict: CandidateRefused, Returns: ReturnsToController, Refused: "x"},
|
|
} {
|
|
if _, err := EncodeHaveNode(HaveNode{Candidates: map[string]CandidateAnswer{"g": a}}); err == nil {
|
|
t.Errorf("%s: taken", name)
|
|
}
|
|
}
|
|
})
|
|
|
|
}
|