package catalogue import ( "encoding/json" "fmt" "strconv" "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}} {{else if eq .shows "state"}}[[block]] block = "custom" command = {{quote (printf "show %s %s" .options.state (or .options.key .machine))}} {{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) { c := block(`{"bar":"bottom","place":"status","shows":"battery"}`) c.IfCapability = "battery" power := Manifest{Module: "power", Contributions: []SeatContribution{c}} 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(), power}} if bar := composedFile(t, r, "a-bar.bottom"); strings.Contains(bar, `block = "battery"`) != c.has { t.Errorf("with %v: the battery placed is not %v:\n%s", c.caps, c.has, bar) } } } func TestIfACapabilityNamesOneTheNodeEngineDetectsOnAnOfferedKindOnly(t *testing.T) { cases := map[string]string{ // A name nothing detects would leave the piece out on every machine. `{"seat":"node-bar","kind":"block","if-capability":"batery","data":{"bar":"top","place":"status","shows":"battery"}}`: "which no machine reports", // A kind the holder depends on is never left out for want of something. `{"seat":"node-hotkeys","kind":"trigger","if-capability":"battery","content":"KEY_BATTERY 1 x"}`: "only a kind the seat offers", `{"seat":"node-backup","kind":"backup","if-capability":"seat","content":"path /x"}`: "only a kind the seat offers", } 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) } } for _, name := range []string{"battery", "power-meter", "seat"} { if !oneOf(KnownCapabilities, name) { t.Errorf("%s is not a known capability", name) } } } func TestResolveCarriesTheMachinesCapabilities(t *testing.T) { node := workstation() node.Capabilities["battery"] = true r, err := Resolve(shelf(Manifest{Module: "a"}), []string{"a"}, node, World{}) if err != nil { t.Fatal(err) } if !r.Capabilities["battery"] || !r.Capabilities["seat"] { t.Fatalf("the resolution carries %v, not the node's %v", r.Capabilities, node.Capabilities) } } func TestAPieceTheHolderCannotRenderIsLeftOutAndNamedNotTheMachine(t *testing.T) { // A shows the shape gained after the holder's template was written: built here, past the check. power := Manifest{Module: "power", Contributions: []SeatContribution{ block(`{"bar":"bottom","place":"status","shows":"gpu"}`), block(`{"bar":"bottom","place":"status","shows":"battery"}`), }} r := Resolution{Node: "n", Account: "op", Modules: []Manifest{barHolder(), power}} c, err := r.Compose(Rendering{}) if err != nil { t.Fatalf("one piece failed the whole machine: %v", err) } if len(c.Unplaced) != 1 || !strings.Contains(c.Unplaced[0], "power's node-bar:block") || !strings.Contains(c.Unplaced[0], "shows=gpu") || !strings.Contains(c.Unplaced[0], "renders nothing") { t.Fatalf("unplaced: %v", c.Unplaced) } for _, res := range c.Resources { if res["id"] == "a-bar.bottom" && !strings.Contains(res["content"].(string), `block = "battery"`) { t.Fatalf("the piece beside it was lost too: %s", res["content"]) } } } func TestTheCataloguesBarRendersEveryExampleOfTheShape(t *testing.T) { holder := catalogueManifest(t, "i3status-rust") s, _ := SeatNamed(BarSeat) if !placesKind(holder, s, BarKindBlock) { t.Skip("the catalogue beside this checkout has a bar that places no blocks yet") } tmpl, err := holderTemplate(holder, s, BarKindBlock) if err != nil { t.Fatal(err) } for _, ex := range s.Receives[0].Shape.Examples { out, err := renderPiece(tmpl, "example", "a-machine", ex) if err != nil || !strings.Contains(out, "[[block]]") { t.Errorf("the catalogue's bar renders %v as %q: %v", ex, out, err) } } for shows := range barShows { covered := false for _, ex := range s.Receives[0].Shape.Examples { covered = covered || ex["shows"] == shows } if !covered { t.Errorf("the shape has no example that shows %s, so no holder is made to render it", shows) } } } func TestQuoteIsATomlBasicStringOfTheSameText(t *testing.T) { for _, in := range []string{`plain`, `say "hi" \ there`, "tab\tnew\nline", "del\x7fend", "nul\x00 esc\x1b", "<&> é ☃"} { q, err := quote(in) if err != nil { t.Fatal(err) } got, err := tomlBasicString(q) if err != nil || got != in { t.Errorf("quote(%q) = %s, which TOML reads as %q: %v", in, q, got, err) } } } // tomlBasicString reads one TOML basic string, as the TOML specification (1.0, "String") defines it: // any character but a quote, a backslash and the control characters U+0000–U+0008, U+000A–U+001F // and U+007F, which are written as escapes. func tomlBasicString(s string) (string, error) { if len(s) < 2 || s[0] != '"' || s[len(s)-1] != '"' { return "", fmt.Errorf("not quoted") } var b strings.Builder rs := []rune(s[1 : len(s)-1]) for i := 0; i < len(rs); i++ { r := rs[i] switch { case r == '"': return "", fmt.Errorf("a bare quote at %d", i) case r == 0x7f || (r < 0x20 && r != '\t'): return "", fmt.Errorf("a bare control character %U at %d", r, i) case r != '\\': b.WriteRune(r) continue } i++ if i >= len(rs) { return "", fmt.Errorf("an escape at the end") } switch rs[i] { case 'b': b.WriteRune('\b') case 't': b.WriteRune('\t') case 'n': b.WriteRune('\n') case 'f': b.WriteRune('\f') case 'r': b.WriteRune('\r') case '"': b.WriteRune('"') case '\\': b.WriteRune('\\') case 'u', 'U': n := 4 if rs[i] == 'U' { n = 8 } if i+n > len(rs)-1 { return "", fmt.Errorf("a short \\u escape") } v, err := strconv.ParseUint(string(rs[i+1:i+1+n]), 16, 32) if err != nil { return "", err } b.WriteRune(rune(v)) i += n default: return "", fmt.Errorf("the escape \\%c is not TOML's", rs[i]) } } return b.String(), nil } 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) } } } func TestAStateBlockIsRenderedForThisMachineAndTheHolderIsGrantedToReadIt(t *testing.T) { power := Manifest{Module: "power", State: []StateDeclaration{{Name: "draw"}}, Contributions: []SeatContribution{ block(`{"bar":"bottom","place":"status","shows":"state","options":{"state":"power.draw"}}`), }} r := Resolution{Node: "laptop", Account: "op", Modules: []Manifest{barHolder(), power}} if bar := composedFile(t, r, "a-bar.bottom"); !strings.Contains(bar, `command = "show power.draw laptop"`) { t.Fatalf("the block does not read this machine's key:\n%s", bar) } if got := ReadsGranted(barHolder(), []Manifest{barHolder(), power}, "laptop"); len(got) != 1 || got[0] != (StateRead{Read: "power.draw", Key: "laptop"}) { t.Fatalf("the holder is granted %v", got) } // Only the holder, and only for what the modules on its machine offer. if got := ReadsGranted(power, []Manifest{barHolder(), power}, "laptop"); len(got) != 0 { t.Fatalf("a contributor is granted %v", got) } if got := ReadsGranted(barHolder(), []Manifest{barHolder()}, "laptop"); len(got) != 0 { t.Fatalf("a holder alone is granted %v", got) } } func TestAModuleOffersItsOwnStateOnly(t *testing.T) { block := `{"seat":"node-bar","kind":"block","data":{"bar":"bottom","place":"status","shows":"state","options":{"state":"%s"}}}` cases := map[string]string{ `{"module":"power","state":["draw"],"contributions":[` + fmt.Sprintf(block, "other.draw") + `]}`: "which power does not keep", `{"module":"power","contributions":[` + fmt.Sprintf(block, "power.draw") + `]}`: "which power does not keep", `{"module":"power","state":["draw"],"contributions":[` + fmt.Sprintf(block, "draw") + `]}`: "does not name a module", } for raw, want := range cases { if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), want) { t.Errorf("%s: accepted, or refused without %q: %v", raw, want, err) } } ok := `{"module":"power","state":["draw"],"contributions":[` + fmt.Sprintf(block, "power.draw") + `]}` if _, err := ParseManifest([]byte(ok)); err != nil { t.Errorf("its own state was refused: %v", err) } } func TestPerMachineStateIsDeclaredAndKeptInItsLongForm(t *testing.T) { m, err := ParseManifest([]byte(`{"module":"power","state":[{"name":"draw","ttl-seconds":30,"per-machine":true}]}`)) if err != nil || len(m.State) != 1 || !m.State[0].PerMachine { t.Fatalf("%+v %v", m.State, err) } raw, _ := json.Marshal(StateDeclaration{Name: "draw", PerMachine: true}) if string(raw) != `{"name":"draw","per-machine":true}` { t.Fatalf("%s", raw) } }