Files
mesh-catalog/modules/i3/cmd/i3-tools/manifest_test.go
T
jochen 1d4de00603 i3: other modules' lines are contributions to node-display-session (hq ADR 0212)
rofi, clipmenu, feh, i3status-rust and the laptop's model module wrote files into i3's config.d,
naming no dependency on the window manager. They now contribute their lines; i3 places them under a
line naming each module, and config.d is the operator's alone. The catalogue-wide test composes the
contributions as the controller does and checks the whole with i3 -C.
2026-10-05 10:11:27 +02:00

231 lines
8.0 KiB
Go

package main
// i3's shape (novox/hq ADR 0208): it holds node-display-session and requires the X display on its own
// machine; it contributes the session's exec to the last xinitrc slot and the desktop's identity to
// the environment; it owns the main configuration, which ends by including the drop-in directory, and
// the login manager's session entry, which runs the session's start.
import (
"encoding/json"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"testing"
)
type manifest struct {
Capabilities []string `json:"capabilities"`
Requires []string `json:"requires"`
Claims []struct {
Name string `json:"name"`
Scope string `json:"scope"`
Serves []string `json:"serves"`
} `json:"claims"`
Seats any `json:"seats"`
Tools []string
Environment struct {
Variables map[string]string `json:"variables"`
} `json:"environment"`
Shell []struct {
For, Slot, Code string
} `json:"shell"`
Resources []map[string]any `json:"resources"`
Build struct {
Artifacts []map[string]any `json:"artifacts"`
} `json:"build"`
}
func readManifest(t *testing.T) manifest {
raw, err := os.ReadFile(filepath.Join("..", "..", "module.json"))
if err != nil {
t.Fatal(err)
}
var m manifest
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
return m
}
func resource(t *testing.T, m manifest, id string) map[string]any {
for _, r := range m.Resources {
if r["id"] == id {
return r
}
}
t.Fatalf("no resource %s", id)
return nil
}
func TestItHoldsTheSessionSeatOnAMachineWithAnXDisplay(t *testing.T) {
m := readManifest(t)
if m.Seats != nil || len(m.Claims) != 1 || m.Claims[0].Name != "node-display-session" || strings.Join(m.Claims[0].Serves, ",") != "reload,workspaces,windows" {
t.Fatalf("%+v", m.Claims)
}
if strings.Join(m.Requires, ",") != "x11-display" {
t.Fatalf("requires %v", m.Requires)
}
served := map[string]bool{}
for _, tool := range tools(&i3{}) {
served[tool.Name] = true
}
for _, v := range m.Claims[0].Serves {
if !served["node-display-session."+v] {
t.Errorf("the seat's %s is not served", v)
}
}
if len(served) != len(m.Tools)+3 {
t.Fatalf("served %d, listed %d", len(served), len(m.Tools))
}
for _, n := range m.Tools {
if !served[n] || !strings.HasPrefix(n, "i3_") {
t.Errorf("%s", n)
}
}
}
func TestItContributesTheSessionsExecLastAndTheDesktopsIdentity(t *testing.T) {
m := readManifest(t)
if len(m.Shell) != 1 || m.Shell[0].For != "xinitrc" || m.Shell[0].Slot != "last" {
t.Fatalf("%+v", m.Shell)
}
var code []string
for _, l := range strings.Split(strings.TrimSpace(m.Shell[0].Code), "\n") {
if !strings.HasPrefix(l, "#") {
code = append(code, l)
}
}
if len(code) != 1 || !strings.HasPrefix(code[0], "exec i3") {
t.Fatalf("one line, the exec: %q", code)
}
if v := m.Environment.Variables; len(v) != 2 || v["XDG_CURRENT_DESKTOP"] != "i3" || v["XDG_SESSION_DESKTOP"] != "i3" {
t.Fatalf("%v", v)
}
}
func TestTheConfigurationIsTheModulesFileImprovedAndEndsWithTheDropIns(t *testing.T) {
m := readManifest(t)
r := resource(t, m, "config")
raw, _ := os.ReadFile(filepath.Join("..", "..", "config", "config"))
if r["content"] != string(raw) || r["path"] != "${machine:account-home}/.config/i3/config" || r["into"] != nil {
t.Fatal("the manifest carries config/config whole, as an owned file")
}
c := string(raw)
lines := strings.Split(strings.TrimSpace(c), "\n")
if lines[len(lines)-1] != "include ~/.config/i3/config.d/*.conf" {
t.Fatal("the drop-ins are read last, with every variable in scope")
}
var lines2 []string
for _, l := range lines {
if !strings.HasPrefix(strings.TrimSpace(l), "#") {
lines2 = append(lines2, l)
}
}
code := strings.Join(lines2, "\n")
for _, gone := range []string{"lxpolkit", "xdg-desktop-portal", "xrdb", "Hack Nerd Font", "refresh_i3status", "rice_set", "exec xterm",
"exec --no-startup-id picom", "exec --no-startup-id nm-applet", "exec --no-startup-id blueman-applet", "exec --no-startup-id nextcloud", "hal/",
// carried by their own modules' drop-ins: rofi, clipmenu, feh, i3status-rust, gnome-keyring
"rofi", "greenclip", "$mod+period", "powermenu", "theme-picker", ".fehbg", "bar {", "i3status-rs", "unlock-keyring"} {
if strings.Contains(code, gone) {
t.Errorf("the configuration still holds %q", gone)
}
}
if !strings.Contains(c, "\nfont pango:JetBrainsMono Nerd Font 11\n") || !strings.Contains(c, "exec --no-startup-id dex --autostart --environment i3") {
t.Fatal("the chosen face for titles; XDG autostart through dex")
}
if !strings.Contains(c, "bindsym $mod+Delete exec --no-startup-id loginctl lock-session") {
t.Fatal("the lock key goes through logind, which the lock screen's module relies on")
}
if i3, err := exec.LookPath("i3"); err == nil {
// As a node with no contribution and no file of the operator's would hold it.
alone := strings.Replace(strings.Replace(c, "${contribution:node-display-session:config}", "", 1),
"\ninclude ~/.config/i3/config.d/*.conf", "\n", 1)
path := filepath.Join(t.TempDir(), "config")
os.WriteFile(path, []byte(alone), 0o644)
out, err := exec.Command(i3, "-C", "-c", path).CombinedOutput()
if err != nil || len(ParseCheck(string(out))) > 0 {
t.Fatalf("i3 -C: %v %s", err, out)
}
}
}
func TestTheLoginManagersEntryRunsTheSessionsStart(t *testing.T) {
m := readManifest(t)
r := resource(t, m, "session")
if r["path"] != "/etc/lemurs/wms/i3" || r["mode"] != "0755" {
t.Fatalf("%v", r)
}
if !strings.Contains(r["content"].(string), `exec systemd-cat -t x-session /bin/sh "$HOME/.xinitrc"`) {
t.Fatal("the entry runs the session's start, never i3 bare")
}
pkgs := map[string]bool{}
for _, x := range m.Resources {
if x["type"] == "package" {
pkgs[x["package"].(string)] = true
}
}
if !pkgs["i3-wm"] || !pkgs["dex"] || len(pkgs) != 2 {
t.Fatalf("%v", pkgs)
}
a := m.Build.Artifacts[0]
if a["from"] != "cmd/i3-tools" || a["binary"] != "i3-tools" || a["language"] != "go" {
t.Fatalf("a binary not named i3, so nothing that looks for i3 by name finds the tools: %v", a)
}
}
// Every module of the catalogue that contributes window-manager lines (novox/hq ADR 0212) is placed in
// the main file as the controller places them — in module order, each module's under a line naming it
// — and loaded as i3 would load them on a machine with all of them assigned: no two bind one key, and
// every line parses.
func TestTheMainFileAndEveryModulesContributionLoadTogether(t *testing.T) {
i3, err := exec.LookPath("i3")
if err != nil {
t.Skip("no i3 here to check with")
}
dir := t.TempDir()
manifests, _ := filepath.Glob(filepath.Join("..", "..", "..", "*", "module.json"))
sort.Strings(manifests)
var placed strings.Builder
var found []string
for _, p := range manifests {
raw, _ := os.ReadFile(p)
var m struct {
Module string `json:"module"`
Contributions []struct {
Seat, Kind, Content string
} `json:"contributions"`
}
json.Unmarshal(raw, &m)
named := false
for _, c := range m.Contributions {
if c.Seat != "node-display-session" || c.Kind != "config" {
continue
}
if !named {
placed.WriteString("# " + m.Module + "\n")
named = true
found = append(found, m.Module)
}
placed.WriteString(c.Content)
if !strings.HasSuffix(c.Content, "\n") {
placed.WriteString("\n")
}
}
}
if len(found) < 5 {
t.Fatalf("expected the launcher, clipboard, wallpaper, bars and the laptop to contribute; found %v", found)
}
main, _ := os.ReadFile(filepath.Join("..", "..", "config", "config"))
text := strings.Replace(string(main), "${contribution:node-display-session:config}", placed.String(), 1)
// The operator's own files are not the catalogue's: the include line itself goes (a contribution may
// name it in a comment, so only the line that is exactly it).
text = strings.Replace(text, "\ninclude ~/.config/i3/config.d/*.conf", "\n", 1)
os.WriteFile(filepath.Join(dir, "config"), []byte(text), 0o644)
out, err := exec.Command(i3, "-C", "-c", filepath.Join(dir, "config")).CombinedOutput()
if err != nil || len(ParseCheck(string(out))) > 0 {
t.Fatalf("with the contributions of %v: %v\n%s", found, err, out)
}
}