Files
mesh-controller/internal/catalogue/collections_l3_test.go
T

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)
}
}