Files
mesh-controller/internal/catalogue/seat_data_test.go
T
jochen 8984c3437f
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery superseded: a newer delivery to the same trunk took over its walk
Leave out a block its holder cannot render, and keep if-capability to known names on offered kinds (hq ADR 0255)
A piece whose shows the holder's template does not know rendered as nothing and failed
the whole machine's declaration; it is now left out and named, for push, plan and the
merge gate. quote escapes DEL, which TOML refuses bare. An if-capability nothing
detects, or on a kind a holder depends on, would drop a piece silently, so both are
refused.
2026-10-08 15:20:59 +02:00

355 lines
15 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}}
{{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", 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)
}
}
}