A module adding a battery to the bar had to write i3status-rust's TOML, so a second bar could not take its place. node-bar now receives a bar-neutral block: the contributor says what it shows, the holder renders it with its own template, places every bar and place once, and a contribution may name a capability the machine must report. The block is offered: the power module runs on servers without a bar.
204 lines
10 KiB
Go
204 lines
10 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|