Seed the eleven node seats with the verbs they start with. A provision may have the machine's reach: a requirement for it resolves only to a provider in the node's own set, is never pulled in, and is refused naming who could. A shell contribution's for gains xinitrc and xresources, placed only by the holder of node-display-server.
214 lines
9.1 KiB
Go
214 lines
9.1 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"encoding/json"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Defends novox/hq ADR 0208: the graphical session is one module per piece, on the mesh's seats.
|
|
|
|
// §2: the eleven roles are the mesh's own node seats, each with the verbs it starts with.
|
|
func TestTheGraphicalSessionsSeatsAreTheMeshsOwnWithTheirVerbs(t *testing.T) {
|
|
want := map[string][]string{
|
|
LoginManagerSeat: {"sessions"},
|
|
DisplayServerSeat: {"displays", "layout"},
|
|
DisplaySessionSeat: {"reload", "workspaces", "windows"},
|
|
TerminalEmulatorSeat: {"open"},
|
|
LauncherSeat: {"menu"},
|
|
NotifierSeat: {"send", "history"},
|
|
LockScreenSeat: {"lock"},
|
|
ClipboardSeat: {"history", "copy"},
|
|
BarSeat: nil,
|
|
CompositorSeat: nil,
|
|
SecretServiceSeat: nil,
|
|
}
|
|
for name, verbs := range want {
|
|
s, ok := SeatNamed(name)
|
|
if !ok {
|
|
t.Errorf("%s is not in the mesh's set", name)
|
|
continue
|
|
}
|
|
if s.Scope != ScopeNode || s.Decision != "novox/hq ADR 0208" {
|
|
t.Errorf("%s is %s-scoped under %q", name, s.Scope, s.Decision)
|
|
}
|
|
var got []string
|
|
for _, v := range s.Serves {
|
|
got = append(got, v.Name)
|
|
if v.Description == "" || v.Input["type"] != "object" {
|
|
t.Errorf("%s.%s has no description or no object schema", name, v.Name)
|
|
}
|
|
}
|
|
if !reflect.DeepEqual(got, verbs) {
|
|
t.Errorf("%s serves %v, want %v", name, got, verbs)
|
|
}
|
|
}
|
|
// The launcher's menu takes a list, and the layout verb says its actions.
|
|
menu, _ := SeatNamed(LauncherSeat)
|
|
choices := menu.Serves[0].Input["properties"].(map[string]any)["choices"].(map[string]any)
|
|
if choices["type"] != "array" {
|
|
t.Errorf("menu's choices are %v, not a list", choices["type"])
|
|
}
|
|
display, _ := SeatNamed(DisplayServerSeat)
|
|
action := display.Serves[1].Input["properties"].(map[string]any)["action"].(map[string]any)
|
|
if !reflect.DeepEqual(action["enum"], []string{"list", "save", "apply"}) {
|
|
t.Errorf("layout's actions are %v", action["enum"])
|
|
}
|
|
// And they survive the store's JSON, which is where the live set comes from.
|
|
if _, err := json.Marshal(graphicalSessionSeats()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// §2: a module may claim one of them, and may not declare it as its own.
|
|
func TestNoModuleMayDeclareAGraphicalSessionSeat(t *testing.T) {
|
|
raw := `{"module":"xorg","seats":[{"name":"node-display-server","scope":"node"}]}`
|
|
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), "mesh's own namespace") {
|
|
t.Fatalf("a module declared node-display-server as its own: %v", err)
|
|
}
|
|
}
|
|
|
|
func displayServer(name, display string, claims ...string) Manifest {
|
|
m := Manifest{Module: name, Provides: []Offer{{Name: display, Reach: ReachMachine}}}
|
|
for _, c := range claims {
|
|
m.Claims = append(m.Claims, Claim{Name: c})
|
|
}
|
|
return m
|
|
}
|
|
|
|
func windowManager() Manifest {
|
|
return Manifest{Module: "i3", Requires: []string{"x11-display"}}
|
|
}
|
|
|
|
// §3: a display is resolved on the requiring module's own node.
|
|
func TestAMachineReachRequirementResolvesToTheProviderOnItsOwnNode(t *testing.T) {
|
|
cat := shelf(windowManager(), displayServer("xorg", "x11-display", DisplayServerSeat))
|
|
got, err := Resolve(cat, []string{"xorg", "i3"}, workstation(), World{})
|
|
if err != nil {
|
|
t.Fatalf("i3 beside xorg did not resolve: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(names(got), []string{"xorg", "i3"}) && !reflect.DeepEqual(names(got), []string{"i3", "xorg"}) {
|
|
t.Errorf("resolved %v", names(got))
|
|
}
|
|
}
|
|
|
|
// §3: never answered by installing a provider, and never by another machine's.
|
|
func TestAMachineReachRequirementIsNotPulledInNorAnsweredFromAnotherNode(t *testing.T) {
|
|
cat := shelf(windowManager(),
|
|
displayServer("xorg", "x11-display", DisplayServerSeat),
|
|
displayServer("xwayland", "x11-display"))
|
|
// Another machine runs xorg and says so to the world; it does not count.
|
|
world := World{Offered: map[string][]Provider{
|
|
"x11-display": {{Node: "laptop", At: "laptop.mesh", Module: "xorg"}}}}
|
|
_, err := Resolve(cat, []string{"i3"}, workstation(), world)
|
|
if err == nil {
|
|
t.Fatal("i3 resolved on a machine with no display of its own")
|
|
}
|
|
for _, want := range []string{
|
|
`"x11-display" is wanted by i3`, "usable only on the machine that provides it",
|
|
"assign one to workstation", "xorg (holds node-display-server)", "xwayland",
|
|
} {
|
|
if !strings.Contains(err.Error(), want) {
|
|
t.Errorf("the refusal does not say %q:\n%v", want, err)
|
|
}
|
|
}
|
|
// With a single provider in the catalogue too: one candidate is still not a choice to make
|
|
// for somebody, unlike a node-scoped provision without the machine's reach.
|
|
_, err = Resolve(shelf(windowManager(), displayServer("xorg", "x11-display", DisplayServerSeat)),
|
|
[]string{"i3"}, workstation(), World{})
|
|
if err == nil {
|
|
t.Fatal("xorg was pulled in for i3")
|
|
}
|
|
// Not in the first pass, whose refusals take the machine off the network.
|
|
if _, err := Resolve(cat, []string{"i3"}, workstation(), World{Unchecked: true}); err != nil {
|
|
t.Errorf("the first pass refused: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestTheMachinesReachIsAProvisionsOnlyReachAndIsNodeScoped(t *testing.T) {
|
|
for _, c := range []struct{ provides, want string }{
|
|
{`{"name":"x11-display","reach":"internal"}`, `with reach "internal"; a provision's reach is "machine" or nothing`},
|
|
{`{"name":"x11-display","scope":"mesh","reach":"machine"}`, `at scope "mesh" with the machine's reach`},
|
|
} {
|
|
_, err := ParseManifest([]byte(`{"module":"xorg","provides":[` + c.provides + `]}`))
|
|
if err == nil || !strings.Contains(err.Error(), c.want) {
|
|
t.Errorf("%s: want %q, got %v", c.provides, c.want, err)
|
|
}
|
|
}
|
|
m, err := ParseManifest([]byte(`{"module":"xorg","provides":[{"name":"x11-display","reach":"machine"}]}`))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !m.Provides[0].MachineReach() {
|
|
t.Fatal("the reach was not read")
|
|
}
|
|
back, _ := json.Marshal(m.Provides[0])
|
|
if string(back) != `{"name":"x11-display","reach":"machine"}` {
|
|
t.Errorf("written back as %s", back)
|
|
}
|
|
}
|
|
|
|
func TestACatalogueDisagreeingAboutAProvisionsReachIsRefused(t *testing.T) {
|
|
cat := shelf(windowManager(), displayServer("xorg", "x11-display"),
|
|
Manifest{Module: "fake-x", Provides: Offers("x11-display")})
|
|
_, err := Resolve(cat, []string{"xorg", "i3"}, workstation(), World{})
|
|
if err == nil || !strings.Contains(err.Error(), `the catalogue disagrees about "x11-display"`) {
|
|
t.Fatalf("a provision with and without the machine's reach gave %v", err)
|
|
}
|
|
}
|
|
|
|
// §4: xinitrc and xresources slots, placed by the display server's holder alone.
|
|
func TestTheSessionsFilesArePlacedByTheDisplayServersHolderAlone(t *testing.T) {
|
|
xorg := Manifest{Module: "xorg", Claims: []Claim{{Name: DisplayServerSeat}},
|
|
Shell: []ShellCode{{For: "xinitrc", Slot: "first", Code: "xset s off"}},
|
|
Resources: []map[string]any{
|
|
{"id": "xinitrc", "type": "file", "path": "/home/op/.xinitrc",
|
|
"content": "${shell:xinitrc:first}${shell:xinitrc:normal}${shell:xinitrc:last}"},
|
|
{"id": "xresources", "type": "file", "path": "/home/op/.Xresources",
|
|
"content": "${shell:xresources:normal}"},
|
|
}}
|
|
i3 := Manifest{Module: "i3", Shell: []ShellCode{{For: "xinitrc", Slot: "last", Code: "exec i3"}}}
|
|
theme := Manifest{Module: "theme", Shell: []ShellCode{
|
|
{For: "xresources", Slot: "normal", Code: "Xft.dpi: 96"},
|
|
{For: "zsh", Slot: "normal", Code: "not for the session"},
|
|
}}
|
|
r := Resolution{Node: "workstation", Account: "op", Modules: []Manifest{xorg, i3, theme}}
|
|
out, err := r.Declaration(Rendering{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
by := map[string]any{}
|
|
for _, res := range out {
|
|
by[res["id"].(string)] = res["content"]
|
|
}
|
|
if got := by["xorg.xinitrc"]; got != "# xorg\nxset s off\n# i3\nexec i3\n" {
|
|
t.Errorf("the .xinitrc is %q", got)
|
|
}
|
|
if got := by["xorg.xresources"]; got != "# theme\nXft.dpi: 96\n" {
|
|
t.Errorf("the .Xresources is %q", got)
|
|
}
|
|
|
|
// The contributions parse; the placeholders parse only in the holder.
|
|
if _, err := ParseManifest([]byte(`{"module":"i3","shell":[{"for":"xinitrc","slot":"last","code":"exec i3"},` +
|
|
`{"for":"xresources","slot":"normal","code":"i3.font: x"}]}`)); err != nil {
|
|
t.Fatalf("a session contribution was refused: %v", err)
|
|
}
|
|
for _, c := range []struct{ claims, content, want string }{
|
|
{``, "${shell:xinitrc:normal}", "does not claim node-display-server; every module's xinitrc is placed by the display server's holder alone"},
|
|
{`{"name":"node-login-shell"}`, "${shell:xresources:normal}", "does not claim node-display-server"},
|
|
{`{"name":"node-display-server"}`, "${shell:zsh:normal}", "does not claim node-login-shell"},
|
|
{`{"name":"node-display-server"}`, "${shell:xsession:normal}", "the session's file one of xinitrc, xresources"},
|
|
} {
|
|
raw := `{"module":"holder","claims":[` + c.claims + `],"resources":[{"id":"rc","type":"file","path":"/etc/rc","content":"` +
|
|
c.content + `"}]}`
|
|
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), c.want) {
|
|
t.Errorf("%s with claims [%s]: want %q, got %v", c.content, c.claims, c.want, err)
|
|
}
|
|
}
|
|
if _, err := ParseManifest([]byte(`{"module":"xorg","claims":[{"name":"node-display-server"}],` +
|
|
`"resources":[{"id":"rc","type":"file","path":"/home/op/.xinitrc","content":"${shell:xinitrc:last}"}]}`)); err != nil {
|
|
t.Errorf("the display server's holder could not place the session's slots: %v", err)
|
|
}
|
|
}
|