Each contribution grain was a manifest field and a renderer of its own; a module now contributes
to any seat with a kind that seat receives, the holder places it with
${contribution:<seat>:<kind>}, and the contribution depends on the seat. node-hotkeys is the
first new seat to receive (triggers); the display session receives window-manager config.
118 lines
4.8 KiB
Go
118 lines
4.8 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Defends novox/hq ADR 0212: a seat says what it receives, its holder places it, and a contribution
|
|
// depends on the seat.
|
|
|
|
func hotkeysHolder() Manifest {
|
|
return Manifest{Module: "triggerhappy", Claims: []Claim{{Name: HotkeysSeat}}, Resources: []map[string]any{
|
|
{"id": "triggers", "type": "file", "path": "/etc/triggerhappy/triggers.d/mesh.conf",
|
|
"content": "# the mesh's triggers\n${contribution:node-hotkeys:trigger}"},
|
|
}}
|
|
}
|
|
|
|
func TestAContributionReachesTheHoldersFileInModuleOrderNamedByModule(t *testing.T) {
|
|
laptop := Manifest{Module: "laptop", Contributions: []SeatContribution{
|
|
{Seat: HotkeysSeat, Kind: "trigger", Content: "KEY_PROG1 1 play ${machine:account-home}"},
|
|
{Seat: HotkeysSeat, Kind: "trigger", Content: "KEY_F21 1 touchpad\n"},
|
|
}}
|
|
another := Manifest{Module: "another", Contributions: []SeatContribution{
|
|
{Seat: HotkeysSeat, Kind: "trigger", Content: "KEY_MUTE 1 mute ${shell:zsh:first}"},
|
|
}}
|
|
unrelated := Manifest{Module: "bar", Contributions: []SeatContribution{
|
|
{Seat: DisplaySessionSeat, Kind: "config", Content: "bar { }"},
|
|
}}
|
|
r := Resolution{Node: "laptop", Account: "op", Modules: []Manifest{hotkeysHolder(), laptop, another, unrelated}}
|
|
out, err := r.Declaration(Rendering{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var file map[string]any
|
|
for _, res := range out {
|
|
if res["id"] == "triggerhappy.triggers" {
|
|
file = res
|
|
}
|
|
}
|
|
if file == nil {
|
|
t.Fatalf("the holder's file was not composed: %v", out)
|
|
}
|
|
// Module order; each module named once; text never read, so neither ${machine:…} nor ${shell:…}
|
|
// inside a contribution is filled.
|
|
want := "# the mesh's triggers\n" +
|
|
"# another\nKEY_MUTE 1 mute ${shell:zsh:first}\n" +
|
|
"# laptop\nKEY_PROG1 1 play ${machine:account-home}\nKEY_F21 1 touchpad\n"
|
|
if got := file["content"]; got != want {
|
|
t.Fatalf("the holder's file is\n%s\nnot\n%s", got, want)
|
|
}
|
|
}
|
|
|
|
func TestNoContributionPlacesNothing(t *testing.T) {
|
|
r := Resolution{Node: "laptop", Account: "op", Modules: []Manifest{hotkeysHolder()}}
|
|
out, err := r.Declaration(Rendering{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, res := range out {
|
|
if res["id"] == "triggerhappy.triggers" && res["content"] != "# the mesh's triggers\n" {
|
|
t.Fatalf("an empty kind left %q", res["content"])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAContributionToASeatThatDoesNotReceiveItIsRefused(t *testing.T) {
|
|
cases := map[string]string{
|
|
`{"seat":"node-hotkeys","kind":"config","content":"x"}`: "it receives trigger",
|
|
`{"seat":"node-nothing","kind":"trigger","content":"x"}`: "the mesh does not define",
|
|
`{"seat":"node-hotkeys","kind":"trigger","content":" "}`: "has no content",
|
|
`{"seat":"node-display-session","kind":"trigger","content":"x"}`: "it receives config",
|
|
}
|
|
for c, want := range cases {
|
|
raw := `{"module":"laptop","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)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAContributionPlaceholderOutsideTheHolderIsRefused(t *testing.T) {
|
|
raw := `{"module":"laptop","resources":[{"id":"t","type":"file","path":"/etc/t","content":"${contribution:node-hotkeys:trigger}"}]}`
|
|
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), "laptop does not claim node-hotkeys") {
|
|
t.Errorf("a placeholder outside the holder was accepted: %v", err)
|
|
}
|
|
raw = `{"module":"triggerhappy","claims":[{"name":"node-hotkeys"}],"resources":[{"id":"t","type":"file","path":"/etc/t","content":"${contribution:node-hotkeys:keys}"}]}`
|
|
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), "it receives trigger") {
|
|
t.Errorf("a placeholder for a kind the seat does not receive was accepted: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAContributionDependsOnItsSeat(t *testing.T) {
|
|
m := Manifest{Module: "laptop", Contributions: []SeatContribution{
|
|
{Seat: HotkeysSeat, Kind: "trigger", Content: "x"},
|
|
{Seat: DisplaySessionSeat, Kind: "config", Content: "y"},
|
|
}}
|
|
if got, want := DependsOn(m), []string{DisplaySessionSeat, HotkeysSeat}; !reflect.DeepEqual(got, want) {
|
|
t.Errorf("depends on %v, want %v", got, want)
|
|
}
|
|
catalogue := map[string]Manifest{"laptop": m, "triggerhappy": hotkeysHolder()}
|
|
if _, err := AssignRefusal(catalogue, "laptop", nil, []string{"laptop"}); err == nil ||
|
|
!strings.Contains(err.Error(), HotkeysSeat) {
|
|
t.Errorf("a contributor without the holder was accepted: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestTheHotkeysSeatIsTheMeshsAndReceivesTriggers(t *testing.T) {
|
|
s, ok := SeatNamed(HotkeysSeat)
|
|
if !ok || s.Scope != ScopeNode || len(s.Receives) != 1 || s.Receives[0].Kind != "trigger" {
|
|
t.Fatalf("%+v %v", s, ok)
|
|
}
|
|
d, _ := SeatNamed(DisplaySessionSeat)
|
|
if len(d.Receives) != 1 || d.Receives[0].Kind != "config" {
|
|
t.Fatalf("the display session receives %+v", d.Receives)
|
|
}
|
|
}
|