lemurs leaves the session a stdout nobody reads, so a program writing to it dies of EPIPE. i3lock-color provides i3lock, so with it still installed the host saw i3lock as present, skipped the install and then removed the only locker; removing it first lets the same apply install i3lock.
206 lines
7.1 KiB
Go
206 lines
7.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"
|
|
"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 {
|
|
out, err := exec.Command(i3, "-C", "-c", filepath.Join("..", "..", "config", "config")).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 drops a file into i3's config.d is loaded with the main file, as
|
|
// i3 would load them on a machine with all of them assigned: no two bind one key, and every line parses.
|
|
func TestTheMainFileAndEveryModulesDropInLoadTogether(t *testing.T) {
|
|
i3, err := exec.LookPath("i3")
|
|
if err != nil {
|
|
t.Skip("no i3 here to check with")
|
|
}
|
|
dir := t.TempDir()
|
|
drop := filepath.Join(dir, "config.d")
|
|
os.MkdirAll(drop, 0o755)
|
|
manifests, _ := filepath.Glob(filepath.Join("..", "..", "..", "*", "module.json"))
|
|
var found []string
|
|
for _, p := range manifests {
|
|
raw, _ := os.ReadFile(p)
|
|
var m struct {
|
|
Resources []map[string]any `json:"resources"`
|
|
}
|
|
json.Unmarshal(raw, &m)
|
|
for _, r := range m.Resources {
|
|
path, _ := r["path"].(string)
|
|
if r["type"] == "file" && strings.Contains(path, "/.config/i3/config.d/") {
|
|
os.WriteFile(filepath.Join(drop, filepath.Base(path)), []byte(r["content"].(string)), 0o644)
|
|
found = append(found, filepath.Base(path))
|
|
}
|
|
}
|
|
}
|
|
main, _ := os.ReadFile(filepath.Join("..", "..", "config", "config"))
|
|
text := strings.Replace(string(main), "include ~/.config/i3/config.d/*.conf", "include "+drop+"/*.conf", 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 drop-ins %v: %v\n%s", found, err, out)
|
|
}
|
|
}
|