Files
mesh-controller/internal/catalogue/seat_data_test.go
T
jschoubben c860426c55
mesh/merge-gate head c860426c555a merged into trunk a20680dfe769 — pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every …
mesh/repo-check head c860426c555a merged into trunk a20680dfe769 — pass: its merge-check.sh passed
mesh/delivery delivered
Narrow the bar test to what the controller owns for a moved bar; rendering is issue 915 (issue 914)
2026-10-16 14:52:13 +02:00

427 lines
19 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"])
}
}
}
// A moved bar is rendered by the provisioner's client, not by the controller, so the rule that its template renders
// every example of the shape is no longer judged here (issue 915, which also covers placementProblems not reading the
// collected form). What the controller still owns is that the bar's files place the blocks at all: a moved bar's
// carry the collected placement, and one that stopped placing them fails here rather than being skipped (issue 432).
func TestTheCataloguesBarPlacesBlocksAndItsShapeCoversEveryShow(t *testing.T) {
holder := catalogueManifest(t, "i3status-rust")
s, _ := SeatNamed(BarSeat)
if holder.Moved {
placed := "${collected:contribution:" + BarSeat + ":" + BarKindBlock + ":"
found := false
for _, res := range holder.Resources {
if content, _ := res["content"].(string); strings.Contains(content, placed) {
found = true
}
}
if !found {
t.Fatalf("the catalogue's moved bar places no blocks: none of its files carries %s…}", placed)
}
} else {
if !placesKind(holder, s, BarKindBlock) {
// It places them since the catalogue's bar took the seat; a skip here hid a bar that stopped
// (novox/hq issue 432).
t.Fatal("the catalogue's bar places no blocks, so none of the shape's examples would render")
}
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(nil, power) {
if seat == BarSeat {
t.Fatalf("a block made the power module depend on the bar: %v", DependsOn(nil, 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)
}
}