package catalogue import ( "encoding/json" "strings" "testing" ) // Defends novox/hq ADR 0255: a seat may receive a kind as data in its own shape, which the holder // renders with its template; the holder places every combination of the shape's placing fields once; // a contribution may be placed only where the machine reports a capability; and an offered kind makes // no dependency on its seat. const barTemplate = `{{if eq .shows "battery"}}[[block]] block = "battery" {{with .options}}{{with .device}}device = {{quote .}} {{end}}{{end}}{{else if eq .shows "command"}}[[block]] block = "custom" command = {{quote .options.command}} interval = {{.options.interval}} {{end}}` func barHolder() Manifest { place := func(bar string) string { return "own-first\n${contribution:node-bar:block:bar=" + bar + ",place=resources}" + "own-middle\n${contribution:node-bar:block:bar=" + bar + ",place=status}own-last\n" } return Manifest{Module: "a-bar", Claims: []Claim{{Name: BarSeat, Renders: map[string]string{BarKindBlock: barTemplate}}}, Resources: []map[string]any{ {"id": "bottom", "type": "file", "path": "/b", "content": place("bottom")}, {"id": "top", "type": "file", "path": "/t", "content": place("top")}, }} } func block(data string) SeatContribution { var d map[string]any if err := json.Unmarshal([]byte(data), &d); err != nil { panic(err) } return SeatContribution{Seat: BarSeat, Kind: BarKindBlock, Data: d} } func composedFile(t *testing.T, r Resolution, id string) string { t.Helper() out, err := r.Declaration(Rendering{}) if err != nil { t.Fatal(err) } for _, res := range out { if res["id"] == id { return res["content"].(string) } } t.Fatalf("%s was not composed", id) return "" } func TestABlockIsRenderedByTheHolderInItsPlaceAndOrderNamedByModule(t *testing.T) { power := Manifest{Module: "power", Contributions: []SeatContribution{ block(`{"bar":"bottom","place":"status","shows":"battery"}`), block(`{"bar":"bottom","place":"status","order":20,"shows":"command","options":{"command":"draw \"now\"","interval":5}}`), }} other := Manifest{Module: "another", Contributions: []SeatContribution{ block(`{"bar":"bottom","place":"status","order":70,"shows":"battery","options":{"device":"BAT1"}}`), block(`{"bar":"top","place":"resources","shows":"command","options":{"command":"up","interval":60}}`), }} r := Resolution{Node: "laptop", Account: "op", Modules: []Manifest{barHolder(), power, other}} // Ordered by order (20, 50, 70), then module order; each piece under its module's name; the // command quoted so its own quotes cannot end the line. want := "own-first\nown-middle\n" + "# power\n[[block]]\nblock = \"custom\"\ncommand = \"draw \\\"now\\\"\"\ninterval = 5\n" + "# power\n[[block]]\nblock = \"battery\"\n" + "# another\n[[block]]\nblock = \"battery\"\ndevice = \"BAT1\"\n" + "own-last\n" if got := composedFile(t, r, "a-bar.bottom"); got != want { t.Fatalf("the bottom bar is\n%s\nnot\n%s", got, want) } want = "own-first\n# another\n[[block]]\nblock = \"custom\"\ncommand = \"up\"\ninterval = 60\nown-middle\nown-last\n" if got := composedFile(t, r, "a-bar.top"); got != want { t.Fatalf("the top bar is\n%s\nnot\n%s", got, want) } } func TestAContributionIfACapabilityIsPlacedOnlyWhereTheMachineReportsIt(t *testing.T) { power := Manifest{Module: "power", Contributions: []SeatContribution{ func() SeatContribution { c := block(`{"bar":"bottom","place":"status","shows":"battery"}`) c.IfCapability = "battery" return c }(), {Seat: HotkeysSeat, Kind: "trigger", Content: "KEY_BATTERY 1 x", IfCapability: "battery"}, }} for _, c := range []struct { caps map[string]bool has bool }{{map[string]bool{"battery": true}, true}, {map[string]bool{"seat": true}, false}, {nil, false}} { r := Resolution{Node: "n", Account: "op", Capabilities: c.caps, Modules: []Manifest{barHolder(), hotkeysHolder(), power}} bar, keys := composedFile(t, r, "a-bar.bottom"), composedFile(t, r, "triggerhappy.triggers") if strings.Contains(bar, `block = "battery"`) != c.has || strings.Contains(keys, "KEY_BATTERY") != c.has { t.Errorf("with %v: the battery placed is not %v:\n%s\n%s", c.caps, c.has, bar, keys) } } } func TestABlockOutsideTheShapeIsRefused(t *testing.T) { cases := map[string]string{ `{"seat":"node-bar","kind":"block","content":"[[block]]"}`: "has no data", `{"seat":"node-bar","kind":"block","content":"x","data":{"bar":"top","place":"status","shows":"battery"}}`: "has content", `{"seat":"node-hotkeys","kind":"trigger","data":{"a":"b"}}`: "receives trigger as text", `{"seat":"node-bar","kind":"block","data":{"bar":"left","place":"status","shows":"battery"}}`: `bar "left"`, `{"seat":"node-bar","kind":"block","data":{"bar":"top","place":"middle","shows":"battery"}}`: `place "middle"`, `{"seat":"node-bar","kind":"block","data":{"bar":"top","place":"status","shows":"gpu"}}`: "shows one of battery, command", `{"seat":"node-bar","kind":"block","data":{"bar":"top","place":"status","shows":"battery","colour":"red"}}`: `no field "colour"`, `{"seat":"node-bar","kind":"block","data":{"bar":"top","place":"status","shows":"battery","order":100}}`: "whole number from 0 to 99", `{"seat":"node-bar","kind":"block","data":{"bar":"top","place":"status","shows":"command"}}`: "needs the option command", `{"seat":"node-bar","kind":"block","data":{"bar":"top","place":"status","shows":"battery","options":{"x":"y"}}}`: `no option "x"`, `{"seat":"node-bar","kind":"block","data":{"bar":"top","place":"status","shows":"command","options":{"command":"c","interval":0}}}`: "seconds", `{"seat":"node-bar","kind":"block","if-capability":"Has Battery","data":{"bar":"top","place":"status","shows":"battery"}}`: "not a capability", } for c, want := range cases { raw := `{"module":"power","contributions":[` + c + `]}` if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), want) { t.Errorf("%s: accepted, or refused without %q: %v", c, want, err) } } ok := `{"module":"power","contributions":[{"seat":"node-bar","kind":"block","if-capability":"battery",` + `"data":{"bar":"bottom","place":"status","order":10,"shows":"command","options":{"command":"c","interval":5}}}]}` if _, err := ParseManifest([]byte(ok)); err != nil { t.Errorf("a block in the shape was refused: %v", err) } } func TestAHolderPlacesEveryCombinationOnceWithATemplateThatRendersEveryExample(t *testing.T) { holder := func(template string, contents ...string) string { var res []string for i, c := range contents { b, _ := json.Marshal(c) res = append(res, `{"id":"f`+string(rune('a'+i))+`","type":"file","path":"/f`+string(rune('a'+i))+`","content":`+string(b)+`}`) } tb, _ := json.Marshal(template) renders := "" if template != "" { renders = `,"renders":{"block":` + string(tb) + `}` } return `{"module":"a-bar","claims":[{"name":"node-bar"` + renders + `}],"resources":[` + strings.Join(res, ",") + `]}` } all := "${contribution:node-bar:block:bar=bottom,place=resources}${contribution:node-bar:block:bar=bottom,place=status}" + "${contribution:node-bar:block:bar=top,place=resources}${contribution:node-bar:block:bar=top,place=status}" cases := map[string]string{ holder(barTemplate, "${contribution:node-bar:block:bar=bottom}"): "never places node-bar:block for bar=top,place=resources", holder(barTemplate, all, "${contribution:node-bar:block:place=status}"): "for bar=bottom,place=status 2 times", holder("", all): "gives no template", holder(`{{if eq .shows "battery"}}b{{end}}`, all): "renders nothing", holder(`{{.nothing}}`, all): "renders nothing", holder(`{{if}`, all): "does not parse", holder(barTemplate, "${contribution:node-bar:block:bar=left}"+all): "bar is one of bottom, top", holder(barTemplate, "${contribution:node-bar:block:colour=red}"+all): "placed by bar, place", holder(barTemplate, "${contribution:node-hotkeys:trigger:bar=top}"): "does not claim node-hotkeys", } for raw, want := range cases { if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), want) { t.Errorf("%s:\naccepted, or refused without %q: %v", raw, want, err) } } for _, raw := range []string{holder(barTemplate, all), holder(barTemplate, "${contribution:node-bar:block}"), // A holder that places no block at all is older than the kind, and is not refused for it. holder("", "no blocks here")} { if _, err := ParseManifest([]byte(raw)); err != nil { t.Errorf("%s: refused: %v", raw, err) } } } func TestAnOfferedBlockMakesNoDependencyOnTheBar(t *testing.T) { power := Manifest{Module: "power", Contributions: []SeatContribution{ block(`{"bar":"bottom","place":"status","shows":"battery"}`), {Seat: HotkeysSeat, Kind: "trigger", Content: "x"}, }} for _, seat := range DependsOn(power) { if seat == BarSeat { t.Fatalf("a block made the power module depend on the bar: %v", DependsOn(power)) } } // And on a machine with no bar it is placed nowhere, and nothing fails. r := Resolution{Node: "server", Account: "op", Capabilities: map[string]bool{"battery": true}, Modules: []Manifest{hotkeysHolder(), power}} if _, err := r.Declaration(Rendering{}); err != nil { t.Fatal(err) } } func TestTheBarReceivesBlocksAsData(t *testing.T) { s, _ := SeatNamed(BarSeat) if len(s.Receives) != 1 || s.Receives[0].Kind != BarKindBlock || s.Receives[0].Shape == nil || !s.Receives[0].Offered { t.Fatalf("node-bar receives %+v", s.Receives) } for _, ex := range s.Receives[0].Shape.Examples { if p := barBlockProblems(ex); len(p) > 0 { t.Errorf("the shape's own example %v is refused: %v", ex, p) } } }