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 =", "${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) } }