Let a seat receive blocks as data its holder renders, placed where the machine has the hardware (hq ADR 0255)
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.
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user