openrazer (the official driver, daemon and library), polychromatic (the AUR tray, kept as found; its i3 line moves out of the i3 module into its own node-display-session contribution), forticlient (the AUR VPN client: its service declared, its configuration never read) and nm-applet (the desktop half of NetworkManager, apart from the server-side module). The openrazer daemon fails on both workstations because the account is not in the openrazer group; openrazer_check names it and the README carries the one-off step, since the account's user resource is zsh's. desktop.go learns to tell a program from another sharing its 15-character command name, so polychromatic's tools never count or end themselves, and a copies test holds the six carriers to one text.
233 lines
8.1 KiB
Go
233 lines
8.1 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",
|
|
// the peripherals' tray: polychromatic's contribution
|
|
"polychromatic"} {
|
|
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)
|
|
}
|
|
}
|