Files
mesh-controller/internal/catalogue/collections_l3_test.go
T
mesh-tools 97a2d841c7 Land feature 57's controller work on its release branch
The controller plans config, templates and moved providers' provisions,
hands bus accounts to the vault and judges the new manifest words, on the
feature's SDK and node-engine validator (feature 57, config and provisioning).
2026-10-15 21:27:33 +00:00

159 lines
8.4 KiB
Go

package catalogue
import (
"reflect"
"strings"
"testing"
)
// An item may carry attributes and a capability, a module may give several items to one collection, and a
// placeholder may select by attributes (design 57 L3, issue 777). The controller leaves out of a plan an item whose
// capability the machine lacks, accepts a selecting placeholder where it accepts the collection, and renders a
// selected moved item into an old placeholder exactly as the contribution it replaces.
const (
barCollector = `{"module":"bars","version":"1","moved":true,
"collections":[{"name":"blocks","comment":"#"}],
"build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]},
"resources":[{"id":"conf","type":"file","path":"/etc/bars.conf","template":"files/bars"}]}`
barsText = "top\n${collected:blocks:bar=top,place=status}bottom\n${collected:blocks:bar=bottom,place=status}${collected:blocks}"
barContribute = `{"module":"power","version":"1","moved":true,
"items":[{"collection":"blocks","name":"battery","if-capability":"battery","attributes":{"bar":"bottom","place":"status","order":"20"},"template":"files/battery"},
{"collection":"blocks","name":"clock","attributes":{"bar":"top","place":"status"},"template":"files/clock"}],
"build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]}}`
)
func blocksOn(t *testing.T, caps map[string]bool) Composed {
t.Helper()
bars := held(t, barCollector, builtWith("bars", map[string]string{"bars": barsText}))
power := held(t, barContribute, builtWith("power", map[string]string{"battery": "B\n", "clock": "C\n"}))
r, err := Resolve(shelf(bars, power), []string{"bars", "power"}, workstation(), World{})
if err != nil {
t.Fatal(err)
}
r.Account, r.Capabilities = "op", caps
c, _ := composeWith(t, r, handingTo("store:5000"))
return c
}
func TestAnItemWhoseCapabilityTheMachineLacksIsLeftOutOfItsPlan(t *testing.T) {
with := blocksOn(t, map[string]bool{"battery": true}).Plans["power"]
want := []PlanItem{
{Collection: "blocks", Name: "battery", Template: "battery",
Attributes: map[string]string{"bar": "bottom", "place": "status", "order": "20"}},
{Collection: "blocks", Name: "clock", Template: "clock", Attributes: map[string]string{"bar": "top", "place": "status"}},
}
if !reflect.DeepEqual(with.Items, want) {
t.Fatalf("with the capability the plan carries %+v", with.Items)
}
without := blocksOn(t, map[string]bool{"seat": true}).Plans["power"]
if !reflect.DeepEqual(without.Items, want[1:]) {
t.Fatalf("without it the plan carries %+v", without.Items)
}
}
func TestACollectorSelectingByAttributesCollectsTheCollectionOnce(t *testing.T) {
bars := blocksOn(t, nil).Plans["bars"]
if !reflect.DeepEqual(bars.Collections, []PlanCollection{{Name: "blocks", Comment: "#"}}) {
t.Fatalf("the plan collects %+v", bars.Collections)
}
}
func TestASelectorIsRefusedWhenMalformedOrNamingACollectionNotCollected(t *testing.T) {
for placeholder, want := range map[string]string{
"${collected:blocks:bar}": "", // no equals sign: a collection named blocks:bar
"${collected:blocks:bar=top,bar=bottom}": "names bar twice",
"${collected:blocks:bar=}": "is not <attribute>=<value>",
"${collected:other:bar=top}": `does not collect "other"`,
} {
raw := strings.Replace(barCollector, `"template":"files/bars"`, `"content":"`+placeholder+`"`, 1)
raw = strings.Replace(raw, `"build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]},`, "", 1)
raw = strings.Replace(raw, `"moved":true,`, "", 1)
m, err := ParseManifest([]byte(raw))
if err == nil {
_, err = (Resolution{Node: "n", Modules: []Manifest{m}}).Declaration(Rendering{})
}
if want == "" {
continue
}
if err == nil || !strings.Contains(err.Error(), want) {
t.Errorf("%s: %v, want %q", placeholder, err, want)
}
}
}
func TestItemNamesAttributesAndCapabilitiesAreJudged(t *testing.T) {
item := func(extra string) string {
return `{"module":"p","moved":true,"build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]},` +
`"items":[` + extra + `]}`
}
for items, want := range map[string]string{
`{"collection":"c","name":"a","template":"files/a"},{"collection":"c","template":"files/b"}`: "has no name",
`{"collection":"c","name":"a","template":"files/a"},{"collection":"c","name":"a","template":"files/b"}`: "twice",
`{"collection":"c","name":"Big","template":"files/a"}`: "is not a name",
`{"collection":"c","attributes":{"order":"100"},"template":"files/a"}`: "0 to 99",
`{"collection":"c","attributes":{"bar":"a b"},"template":"files/a"}`: "is not a value",
`{"collection":"c","if-capability":"batery","template":"files/a"}`: "no machine reports",
`{"collection":"contribution:node-bar:block","attributes":{"bar":"left"},"template":"files/a"}`: "bar=left",
`{"collection":"contribution:node-bar:block","attributes":{"colour":"red"},"template":"files/a"}`: `"colour" is none of them`,
`{"collection":"c","name":"a","template":"files/a","colour":"red"}`: "colour",
} {
_, err := ParseManifest([]byte(item(items)))
if err == nil || !strings.Contains(err.Error(), want) {
t.Errorf("%s: %v, want %q", items, err, want)
}
}
ok := item(`{"collection":"contribution:node-bar:block","name":"a","attributes":{"bar":"top","place":"status","order":"5"},"if-capability":"battery","template":"files/a"}`)
if _, err := ParseManifest([]byte(ok)); err != nil {
t.Fatal(err)
}
}
// A moved contributor's items, selected by the old placeholder's pair, render in the old order (order, module, item
// name) and with the same lines as the contributions they replace.
func TestASelectedMovedItemRendersIntoTheOldBarPlaceholderAsTheContributionDid(t *testing.T) {
piece := func(order, command string) string {
return "[[block]]\nblock = \"custom\"\ncommand = \"" + command + "\"\ninterval = 5\n"
}
asOld := Manifest{Module: "power", Contributions: []SeatContribution{
block(`{"bar":"bottom","place":"status","order":20,"shows":"command","options":{"command":"two","interval":5}}`),
block(`{"bar":"bottom","place":"status","shows":"command","options":{"command":"one","interval":5}}`),
block(`{"bar":"top","place":"resources","shows":"command","options":{"command":"up","interval":5}}`),
}}
asItems := Manifest{Module: "power", Moved: true, Items: []Item{
{Collection: "contribution:node-bar:block", Name: "b", Content: piece("", "one"),
Attributes: map[string]string{"bar": "bottom", "place": "status"}},
{Collection: "contribution:node-bar:block", Name: "a", Content: piece("", "two"),
Attributes: map[string]string{"bar": "bottom", "place": "status", "order": "20"}},
{Collection: "contribution:node-bar:block", Name: "c", Content: piece("", "up"),
Attributes: map[string]string{"bar": "top", "place": "resources"}},
{Collection: "contribution:node-bar:block", Name: "d", Content: "x\n", IfCapability: "battery",
Attributes: map[string]string{"bar": "bottom", "place": "status"}},
}}
other := Manifest{Module: "another", Contributions: []SeatContribution{
block(`{"bar":"bottom","place":"status","order":20,"shows":"command","options":{"command":"mid","interval":5}}`)}}
for _, caps := range []map[string]bool{nil, {"battery": true}} {
was := composedFile(t, Resolution{Node: "n", Account: "op", Capabilities: caps,
Modules: []Manifest{barHolder(), asOld, other}}, "a-bar.bottom")
is := composedFile(t, Resolution{Node: "n", Account: "op", Capabilities: caps,
Modules: []Manifest{barHolder(), asItems, other}}, "a-bar.bottom")
// The old form puts the contributions of a module under one line naming it and the item form puts one under
// each item: the same pieces in the same order, otherwise the same bytes.
want := "own-first\nown-middle\n# another\n" + piece("", "mid") + "# power\n" + piece("", "two") +
"# power\n" + piece("", "one")
if caps["battery"] {
want += "# power\nx\n"
}
want += "own-last\n"
if is != want {
t.Errorf("caps %v: the items render\n%s\nnot\n%s", caps, is, want)
}
if !strings.Contains(was, piece("", "two")) {
t.Errorf("the contributions made\n%s", was)
}
}
if got := composedFile(t, Resolution{Node: "n", Account: "op", Modules: []Manifest{barHolder(), asItems}}, "a-bar.top"); !strings.Contains(got, "\"up\"") || strings.Contains(got, `"one"`) {
t.Errorf("the top bar renders\n%s", got)
}
}