301 lines
12 KiB
Go
301 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"adwaita/internal/desktop"
|
|
)
|
|
|
|
type manifest struct {
|
|
Capabilities []string `json:"capabilities"`
|
|
Claims []any `json:"claims"`
|
|
Tools []string `json:"tools"`
|
|
Environment struct {
|
|
Variables map[string]string `json:"variables"`
|
|
} `json:"environment"`
|
|
Items []struct {
|
|
Collection string `json:"collection"`
|
|
Attributes map[string]string `json:"attributes"`
|
|
Template string `json:"template"`
|
|
} `json:"items"`
|
|
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)
|
|
}
|
|
// The module's code and variables are the templates of its items (design 57 L3, issues 795 and 807).
|
|
for _, it := range m.Items {
|
|
text, err := os.ReadFile(filepath.Join("..", "..", it.Template))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
parts := strings.Split(it.Collection, ":")
|
|
switch {
|
|
case len(parts) == 3 && parts[0] == "shell":
|
|
m.Shell = append(m.Shell, struct{ For, Slot, Code string }{parts[1], parts[2], string(text)})
|
|
case it.Collection == "environment" && it.Attributes["kind"] == "variable":
|
|
if m.Environment.Variables == nil {
|
|
m.Environment.Variables = map[string]string{}
|
|
}
|
|
m.Environment.Variables[it.Attributes["name"]] = strings.TrimSuffix(string(text), "\n")
|
|
}
|
|
}
|
|
return m
|
|
}
|
|
|
|
func TestItContributesTheThemesWordsAndClaimsNoSeat(t *testing.T) {
|
|
m := readManifest(t)
|
|
if m.Claims != nil {
|
|
t.Fatal("a theme is not a seat: several coexist")
|
|
}
|
|
want := map[string]string{"GTK_THEME": "Adwaita:dark", "GTK2_RC_FILES": "/usr/share/themes/Adwaita-dark/gtk-2.0/gtkrc",
|
|
"QT_QPA_PLATFORMTHEME": "qt6ct", "QT_STYLE_OVERRIDE": "Fusion", "QT_SELECT": "6", "XCURSOR_THEME": "Adwaita", "XCURSOR_SIZE": "24"}
|
|
if len(m.Environment.Variables) != len(want) {
|
|
t.Fatalf("%v", m.Environment.Variables)
|
|
}
|
|
for k, v := range want {
|
|
if m.Environment.Variables[k] != v {
|
|
t.Errorf("%s=%q", k, m.Environment.Variables[k])
|
|
}
|
|
}
|
|
served := map[string]bool{}
|
|
for _, tool := range tools(adwaita{}) {
|
|
served[tool.Name] = true
|
|
}
|
|
if len(served) != len(m.Tools) {
|
|
t.Fatalf("%v %v", served, m.Tools)
|
|
}
|
|
for _, n := range m.Tools {
|
|
if !served[n] {
|
|
t.Errorf("%s", n)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTheSessionLinesSetGSettingsAndTheResourcesTheCursor(t *testing.T) {
|
|
m := readManifest(t)
|
|
if len(m.Shell) != 2 || m.Shell[0].For != "xresources" || m.Shell[1].For != "xinitrc" || m.Shell[0].Slot != "normal" || m.Shell[1].Slot != "normal" {
|
|
t.Fatalf("%+v", m.Shell)
|
|
}
|
|
if m.Shell[0].Code != "! adwaita: the cursor, for X programs that take it from the resources.\nXcursor.theme: Adwaita\nXcursor.size: 24\n" {
|
|
t.Fatalf("%q", m.Shell[0].Code)
|
|
}
|
|
x := m.Shell[1].Code
|
|
for _, want := range []string{"color-scheme 'prefer-dark'", "gtk-theme 'Adwaita'", "icon-theme 'Adwaita'", "cursor-theme 'Adwaita'", "font-name 'Inter 11'", "monospace-font-name 'JetBrainsMono Nerd Font 11'"} {
|
|
if !strings.Contains(x, want) {
|
|
t.Errorf("lacks %s", want)
|
|
}
|
|
}
|
|
for _, l := range strings.Split(strings.TrimSpace(x), "\n") {
|
|
if !strings.HasPrefix(l, "#") && !strings.HasSuffix(l, "|| true") {
|
|
t.Errorf("a session line that can stop the session's start: %q", l)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestItOwnsTheFilesItsSourcesHoldAndNoQt5Duplicate(t *testing.T) {
|
|
m := readManifest(t)
|
|
sources := map[string]string{"gtk3": "gtk-settings.ini", "gtk4": "gtk-settings.ini", "qt6ct": "qt6ct.conf", "portals": "portals.conf", "cursor": "cursor-index.theme"}
|
|
pkgs := []string{}
|
|
for _, r := range m.Resources {
|
|
id := r["id"].(string)
|
|
if r["type"] == "package" {
|
|
pkgs = append(pkgs, r["package"].(string))
|
|
continue
|
|
}
|
|
_, inline := r["content"]
|
|
if _, err := os.Stat(filepath.Join("..", "..", "files", sources[id])); err != nil || inline ||
|
|
r["template"] != "files/"+sources[id] || r["into"] != nil || r["owner"] != "${machine:account}" {
|
|
t.Errorf("%s is not files/%s, whole, as its template, and the account's", id, sources[id])
|
|
}
|
|
if strings.Contains(r["path"].(string), "qt5ct") {
|
|
t.Error("qt5ct: both workstations run Qt 6 programs through qt6ct; a second tool is a duplicate")
|
|
}
|
|
}
|
|
if strings.Join(pkgs, ",") != "gnome-themes-extra,adwaita-icon-theme,adwaita-cursors,qt6ct,xdg-desktop-portal-gtk" {
|
|
t.Fatalf("%v", pkgs)
|
|
}
|
|
qt := readINI(filepath.Join("..", "..", "files", "qt6ct.conf"))
|
|
if qt["Appearance"]["style"] != "Fusion" || qt["Appearance"]["custom_palette"] != "true" || !strings.Contains(qt["Fonts"]["general"], "Inter,11") {
|
|
t.Fatalf("%v", qt)
|
|
}
|
|
if _, err := os.Stat("/usr/share/qt6ct/colors"); err == nil {
|
|
if _, err := os.Stat(qt["Appearance"]["color_scheme_path"]); err != nil {
|
|
t.Fatalf("qt6ct ships no %s", qt["Appearance"]["color_scheme_path"])
|
|
}
|
|
}
|
|
gtk := readINI(filepath.Join("..", "..", "files", "gtk-settings.ini"))["Settings"]
|
|
if gtk["gtk-theme-name"] != "Adwaita" || gtk["gtk-application-prefer-dark-theme"] != "1" || gtk["gtk-font-name"] != "Inter 11" {
|
|
t.Fatalf("%v", gtk)
|
|
}
|
|
}
|
|
|
|
func TestKeyFilesAreReadWithTheirComments(t *testing.T) {
|
|
ini := ParseINI("# c\n[A]\nk = v\n; also a comment\n[B]\nx=1=2\n")
|
|
if ini["A"]["k"] != "v" || ini["B"]["x"] != "1=2" || len(ini) != 2 {
|
|
t.Fatalf("%v", ini)
|
|
}
|
|
}
|
|
|
|
func TestThePortalsAnswerIsResolvedAsXdgDesktopPortalDoes(t *testing.T) {
|
|
dir := t.TempDir()
|
|
os.WriteFile(filepath.Join(dir, "gtk.portal"), []byte("[portal]\nDBusName=org.freedesktop.impl.portal.desktop.gtk\nInterfaces=org.freedesktop.impl.portal.FileChooser;org.freedesktop.impl.portal.Settings;\nUseIn=gnome\n"), 0o644)
|
|
os.WriteFile(filepath.Join(dir, "gnome-keyring.portal"), []byte("[portal]\nDBusName=org.freedesktop.secrets\nInterfaces=org.freedesktop.impl.portal.Secret;\nUseIn=gnome\n"), 0o644)
|
|
os.WriteFile(filepath.Join(dir, "kde.portal"), []byte("[portal]\nDBusName=org.freedesktop.impl.portal.desktop.kde\nInterfaces=org.freedesktop.impl.portal.FileChooser;\nUseIn=KDE\n"), 0o644)
|
|
b := Backends([]string{dir})
|
|
if len(b) != 3 || b[0].Name != "gnome-keyring" || len(b[1].Interfaces) != 2 {
|
|
t.Fatalf("%+v", b)
|
|
}
|
|
got := Resolve(b, map[string]string{"default": "gtk", "org.freedesktop.impl.portal.Secret": "gnome-keyring"}, []string{"i3"})
|
|
if got["org.freedesktop.impl.portal.FileChooser"] != "gtk" || got["org.freedesktop.impl.portal.Secret"] != "gnome-keyring" || got["org.freedesktop.impl.portal.Settings"] != "gtk" {
|
|
t.Fatalf("%v", got)
|
|
}
|
|
got = Resolve(b, map[string]string{"default": "gtk"}, []string{"i3"})
|
|
if got["org.freedesktop.impl.portal.Secret"] != "(none)" {
|
|
t.Fatalf("gtk does not implement secrets: %v", got)
|
|
}
|
|
got = Resolve(b, map[string]string{"default": "none;gtk"}, nil)
|
|
if got["org.freedesktop.impl.portal.FileChooser"] != "(none)" {
|
|
t.Fatalf("none stops the list: %v", got)
|
|
}
|
|
got = Resolve(b, nil, []string{"KDE"})
|
|
if got["org.freedesktop.impl.portal.FileChooser"] != "kde" || got["org.freedesktop.impl.portal.Settings"] != "(none)" {
|
|
t.Fatalf("with no configuration, UseIn decides: %v", got)
|
|
}
|
|
c := PortalConfigs("/h", []string{"i3", "GNOME"})
|
|
if c[0] != "/h/.config/xdg-desktop-portal/i3-portals.conf" || c[1] != "/h/.config/xdg-desktop-portal/gnome-portals.conf" || c[2] != "/h/.config/xdg-desktop-portal/portals.conf" {
|
|
t.Fatalf("%v", c[:3])
|
|
}
|
|
}
|
|
|
|
func TestThemesAreFoundOnceEachWithCursorsAndIcons(t *testing.T) {
|
|
a, b := t.TempDir(), t.TempDir()
|
|
os.MkdirAll(filepath.Join(a, "Adwaita", "cursors"), 0o755)
|
|
os.MkdirAll(filepath.Join(b, "Adwaita"), 0o755)
|
|
os.WriteFile(filepath.Join(b, "Adwaita", "index.theme"), []byte("[Icon Theme]\nName=Adwaita\nInherits=hicolor\nDirectories=16x16\n"), 0o644)
|
|
os.MkdirAll(filepath.Join(b, "hicolor"), 0o755)
|
|
os.WriteFile(filepath.Join(b, "hicolor", "index.theme"), []byte("[Icon Theme]\nName=Hicolor\nDirectories=16x16\n"), 0o644)
|
|
os.MkdirAll(filepath.Join(b, "empty"), 0o755)
|
|
got := Themes([]string{a, b})
|
|
if len(got) != 2 || got[0].Name != "Adwaita" || !got[0].Cursors || got[0].Icons || got[0].Dir != filepath.Join(a, "Adwaita") || got[1].Name != "hicolor" {
|
|
t.Fatalf("the first directory's Adwaita hides the second's: %+v", got)
|
|
}
|
|
}
|
|
|
|
type fake struct {
|
|
ran []string
|
|
get map[string]string
|
|
}
|
|
|
|
func (f *fake) desk() desktop.Desk {
|
|
return desktop.Desk{
|
|
Find: func() (*desktop.Session, error) { return nil, &desktop.NoSession{Reason: "none"} },
|
|
Run: func(_ context.Context, env []string, _ []byte, name string, args ...string) desktop.Result {
|
|
line := strings.Join(append([]string{name}, args...), " ")
|
|
f.ran = append(f.ran, line)
|
|
switch {
|
|
case name == "gsettings" && args[0] == "get":
|
|
return desktop.Result{Stdout: "'" + f.get[args[2]] + "'\n"}
|
|
case name == "gsettings" && args[0] == "set":
|
|
f.get[args[2]] = args[3]
|
|
case name == "systemctl":
|
|
return desktop.Result{Stdout: "GTK_THEME=Adwaita:dark\nNPM_TOKEN=secret\nXDG_CURRENT_DESKTOP=i3\n"}
|
|
case name == "busctl" && args[1] == "call":
|
|
return desktop.Result{Stdout: "v u 1\n"}
|
|
}
|
|
return desktop.Result{}
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestAppearanceSwitchesGSettingsAndSaysWhatFollowsAndWhatStays(t *testing.T) {
|
|
f := &fake{get: map[string]string{"color-scheme": "prefer-dark", "gtk-theme": "Adwaita"}}
|
|
a := adwaita{d: f.desk(), home: t.TempDir()}
|
|
got, err := a.appearance(context.Background(), desktop.Args{"mode": "light"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
j := asJSON(got)
|
|
if f.get["color-scheme"] != "prefer-light" || !strings.Contains(j, `"switched":"light"`) || !strings.Contains(j, "GTK_THEME=Adwaita:dark") || !strings.Contains(j, `"lasts"`) {
|
|
t.Fatalf("%s", j)
|
|
}
|
|
if strings.Contains(j, "secret") || strings.Contains(j, "NPM_TOKEN") {
|
|
t.Fatal("only the theme's words are read back from the user manager")
|
|
}
|
|
if _, err := a.appearance(context.Background(), desktop.Args{"mode": "blue"}); err == nil {
|
|
t.Fatal("mode is dark or light")
|
|
}
|
|
got, _ = a.appearance(context.Background(), desktop.Args{})
|
|
if strings.Contains(asJSON(got), "switched") {
|
|
t.Fatal("reading changes nothing")
|
|
}
|
|
}
|
|
|
|
func TestACursorThemeMustBeInstalledAndWithoutASessionOnlyGSettingsChanges(t *testing.T) {
|
|
dir := t.TempDir()
|
|
os.MkdirAll(filepath.Join(dir, "Adwaita", "cursors"), 0o755)
|
|
f := &fake{get: map[string]string{}}
|
|
a := adwaita{d: f.desk(), home: t.TempDir(), iconDirs: []string{dir}}
|
|
if _, err := a.cursor(context.Background(), desktop.Args{"theme": "Bibata"}); err == nil {
|
|
t.Fatal("a theme that is not installed")
|
|
}
|
|
got, err := a.cursor(context.Background(), desktop.Args{"theme": "Adwaita", "size": float64(32)})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if f.get["cursor-theme"] != "Adwaita" || f.get["cursor-size"] != "32" || !strings.Contains(asJSON(got), "no graphical session") {
|
|
t.Fatalf("%v %s", f.get, asJSON(got))
|
|
}
|
|
}
|
|
|
|
func asJSON(v any) string {
|
|
b, _ := json.Marshal(v)
|
|
return string(b)
|
|
}
|
|
|
|
// No file resource carries its text inline: each names its template in files/, and the build packs files/
|
|
// into the one archive the controller reads the templates back from (novox/hq ADR 0349 §2).
|
|
func TestEveryFileTakesItsTextFromATemplate(t *testing.T) {
|
|
m := readManifest(t)
|
|
for _, r := range m.Resources {
|
|
if _, inline := r["content"]; inline {
|
|
t.Errorf("resource %v keeps its text inline: keep it in files/ and name it as the template", r["id"])
|
|
}
|
|
if name, ok := r["template"].(string); ok {
|
|
if _, err := os.Stat(filepath.Join("..", "..", name)); err != nil {
|
|
t.Errorf("resource %v names %s: %v", r["id"], name, err)
|
|
}
|
|
}
|
|
}
|
|
packs := 0
|
|
for _, a := range m.Build.Artifacts {
|
|
if a["kind"] == "archive" && a["from"] == "files" {
|
|
packs++
|
|
if a["name"] != "files" {
|
|
t.Errorf("the templates archive is named %v, not files", a["name"])
|
|
}
|
|
}
|
|
}
|
|
if packs != 1 {
|
|
t.Errorf("the build packs files/ into %d archives, not one", packs)
|
|
}
|
|
}
|