430 lines
18 KiB
Go
430 lines
18 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
const home = "/home/op"
|
|
|
|
// fake is a machine with these files under a temporary root; a value starting with "->" is a
|
|
// symbolic link to the rest, and one starting with "#!" is an executable.
|
|
func fake(t *testing.T, files map[string]string) *Machine {
|
|
t.Helper()
|
|
root := t.TempDir()
|
|
for p, c := range files {
|
|
full := filepath.Join(root, p)
|
|
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
switch {
|
|
case strings.HasPrefix(c, "->"):
|
|
if err := os.Symlink(strings.TrimPrefix(c, "->"), full); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
case c == "<dir>":
|
|
if err := os.MkdirAll(full, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
default:
|
|
mode := os.FileMode(0o644)
|
|
if strings.HasPrefix(c, "#!") {
|
|
mode = 0o755
|
|
}
|
|
if err := os.WriteFile(full, []byte(c), mode); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
}
|
|
return &Machine{Root: root, Home: home, UID: os.Getuid(), Desktop: "i3", Timeout: time.Second,
|
|
Run: func(context.Context, []string, string, ...string) (string, error) { return "", nil }}
|
|
}
|
|
|
|
func app(name, exec, mime string) string {
|
|
s := "[Desktop Entry]\nType=Application\nName=" + name + "\nExec=" + exec + "\n"
|
|
if mime != "" {
|
|
s += "MimeType=" + mime + "\n"
|
|
}
|
|
return s
|
|
}
|
|
|
|
// The desktop's folders as found on 2026-10-05, in the module's file with that machine's settings.
|
|
func desktopUserDirs(t *testing.T) string {
|
|
raw, err := os.ReadFile(filepath.Join("..", "..", "files", "user-dirs.dirs"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r := strings.NewReplacer("${config:desktop-dir}", "$HOME/", "${config:publicshare-dir}", "$HOME/", "${config:videos-dir}", "$HOME/")
|
|
return r.Replace(string(raw))
|
|
}
|
|
|
|
func TestTheFoldersAreReadAsXdgUserDirResolvesThem(t *testing.T) {
|
|
conf, _ := os.ReadFile(filepath.Join("..", "..", "files", "user-dirs.conf"))
|
|
m := fake(t, map[string]string{
|
|
home + "/.config/user-dirs.dirs": desktopUserDirs(t),
|
|
home + "/.config/user-dirs.conf": string(conf),
|
|
home + "/.config/user-dirs.locale": "en_US\n",
|
|
"/etc/xdg/user-dirs.conf": "enabled=True\n",
|
|
"/etc/xdg/user-dirs.defaults": "# c\nDESKTOP=Desktop\nDOWNLOAD=Downloads\nPROJECTS=Projects\nNEWTHING=New\n",
|
|
home + "/Desktop": "a file where the folder would be\n",
|
|
home + "/Downloads": "<dir>",
|
|
home + "/projects": "<dir>",
|
|
})
|
|
a := m.UserDirs()
|
|
if !a.Meshs || a.Locale != "en_US" || a.Update.Enabled || a.Update.DecidedBy != "~/.config/user-dirs.conf" {
|
|
t.Fatalf("%+v", a)
|
|
}
|
|
got := map[string]UserDir{}
|
|
for _, d := range a.Dirs {
|
|
got[d.Name] = d
|
|
}
|
|
if len(a.Dirs) != 9 {
|
|
t.Fatalf("%d folders: %+v", len(a.Dirs), a.Dirs)
|
|
}
|
|
if d := got["DESKTOP"]; !d.Home || d.Path != "~" || d.Default != "~/Desktop" {
|
|
t.Errorf("DESKTOP: %+v", d)
|
|
}
|
|
if d := got["DOWNLOAD"]; d.Home || !d.Exists || d.Path != "~/Downloads" {
|
|
t.Errorf("DOWNLOAD: %+v", d)
|
|
}
|
|
if d := got["PROJECTS"]; d.Path != "~/projects" || !d.Exists || d.Default != "~/Projects" {
|
|
t.Errorf("PROJECTS: %+v", d)
|
|
}
|
|
if d := got["MUSIC"]; d.Exists || d.Kind != "absent" {
|
|
t.Errorf("MUSIC: %+v", d)
|
|
}
|
|
if !reflect.DeepEqual(a.Missing, []string{"NEWTHING"}) {
|
|
t.Errorf("defaults without a line: %v", a.Missing)
|
|
}
|
|
}
|
|
|
|
func TestAnUpdateLeftEnabledIsSaid(t *testing.T) {
|
|
m := fake(t, map[string]string{"/etc/xdg/user-dirs.conf": "# c\nenabled=True\nfilename_encoding=UTF-8\n"})
|
|
a := m.UserDirs()
|
|
if !a.Update.Enabled || a.Update.DecidedBy != "/etc/xdg/user-dirs.conf" || !strings.Contains(a.Update.Note, "rewrites the file") {
|
|
t.Fatalf("%+v", a.Update)
|
|
}
|
|
}
|
|
|
|
func TestATypeIsAMimeTypeOrAScheme(t *testing.T) {
|
|
for in, want := range map[string]string{
|
|
"https": "x-scheme-handler/https", "mailto:": "x-scheme-handler/mailto", "http://": "x-scheme-handler/http",
|
|
"text/html": "text/html", "x-scheme-handler/tg": "x-scheme-handler/tg", "application/xhtml+xml": "application/xhtml+xml",
|
|
} {
|
|
if got, err := Normalise(in); err != nil || got != want {
|
|
t.Errorf("%s: %s %v", in, got, err)
|
|
}
|
|
}
|
|
for _, bad := range []string{"", "text/html; rm -rf", "../x", "Text Html"} {
|
|
if _, err := Normalise(bad); err == nil {
|
|
t.Errorf("%q accepted", bad)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTheProgramIsFoundBehindLaunchers(t *testing.T) {
|
|
// Exec lines found on the workstations.
|
|
for exec, want := range map[string]string{
|
|
`env APPIMAGELAUNCHER_DISABLE=1 DESKTOPINTEGRATION=0 /usr/bin/Plexamp.AppImage --disable-seccomp-filter-sandbox`: "/usr/bin/Plexamp.AppImage",
|
|
`sh -c "/opt/forticlient/gui/FortiClient >/dev/null 2>&1"`: "/opt/forticlient/gui/FortiClient",
|
|
`zsh -c 'dockly'`: "dockly",
|
|
`"/opt/claude-code/bin/claude" --handle-uri %u`: "/opt/claude-code/bin/claude",
|
|
`env LIBGL_ALWAYS_SOFTWARE=1 Telegram -- %U`: "Telegram",
|
|
`/usr/bin/slack --gtk-version=3 -s %U`: "/usr/bin/slack",
|
|
`"/path with space/Fleet" %u`: "/path with space/Fleet",
|
|
`steam steam://rungameid/427520`: "steam",
|
|
`xterm -fa monospace -fs 11 -e glow -p %f`: "xterm",
|
|
``: "",
|
|
} {
|
|
if got := Program(exec); got != want {
|
|
t.Errorf("%s: %q, want %q", exec, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestADuplicateGroupOrKeyIsReportedNotMerged(t *testing.T) {
|
|
k := ParseKeyFile("# >>> mesh >>>\n[Default Applications]\nx=a.desktop\n# <<<\n\n[Default Applications]\nx=b.desktop\ny=c\n[Added Associations]\nz=d;\nstray\n")
|
|
if !reflect.DeepEqual(k.DupGroups, []string{"Default Applications"}) || !reflect.DeepEqual(k.DupKeys, []string{"Default Applications: x"}) {
|
|
t.Fatalf("%+v", k)
|
|
}
|
|
if k.Groups["Default Applications"]["x"] != "a.desktop" || k.Groups["Default Applications"]["y"] != "c" || len(k.Unparsable) != 1 {
|
|
t.Fatalf("%+v", k)
|
|
}
|
|
}
|
|
|
|
// A machine with the mesh's list, the person's list, and some applications.
|
|
func mimeMachine(t *testing.T, extra map[string]string) *Machine {
|
|
mesh, err := os.ReadFile(filepath.Join("..", "..", "files", "mimeapps.list"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
files := map[string]string{
|
|
MeshList: string(mesh),
|
|
home + "/.config/mimeapps.list": "[Default Applications]\ntext/plain=code-oss.desktop;\ninode/directory=xplr.desktop\nx-scheme-handler/https=gone.desktop;chromium.desktop;\n",
|
|
"/usr/share/applications/firefox.desktop": app("Firefox", "/usr/lib/firefox/firefox %u", "text/html;x-scheme-handler/https;"),
|
|
"/usr/share/applications/chromium.desktop": app("Chromium", "/usr/bin/chromium %U", "x-scheme-handler/https;"),
|
|
"/usr/share/applications/thunar.desktop": app("Thunar", "thunar %F", "inode/directory;"),
|
|
home + "/.local/share/applications/xplr.desktop": app("xplr", "xplr", "inode/directory;"),
|
|
home + "/.local/share/applications/fleet.desktop": app("Fleet", `"/home/op/fleet/bin/Fleet" %u`, "text/plain;"),
|
|
"/usr/lib/firefox/firefox": "#!/bin/sh\n",
|
|
"/usr/bin/chromium": "#!/bin/sh\n",
|
|
"/usr/bin/thunar": "#!/bin/sh\n",
|
|
}
|
|
for k, v := range extra {
|
|
files[k] = v
|
|
}
|
|
return fake(t, files)
|
|
}
|
|
|
|
func TestTheDefaultIsDecidedInTheSpecificationsOrder(t *testing.T) {
|
|
m := mimeMachine(t, nil)
|
|
// The person's list names an application that is gone, then one that is installed.
|
|
if d := m.Decide("x-scheme-handler/https"); d.ID != "chromium.desktop" || d.File != "~/.config/mimeapps.list" ||
|
|
!reflect.DeepEqual(d.Skipped, []string{"gone.desktop (~/.config/mimeapps.list): not installed"}) {
|
|
t.Fatalf("%+v", d)
|
|
}
|
|
// The person's choice wins over the mesh's.
|
|
if d := m.Decide("inode/directory"); d.ID != "xplr.desktop" {
|
|
t.Fatalf("%+v", d)
|
|
}
|
|
// What the person has not chosen, the mesh's list answers.
|
|
if d := m.Decide("text/html"); d.ID != "firefox.desktop" || d.File != MeshList {
|
|
t.Fatalf("%+v", d)
|
|
}
|
|
// Nothing installed is named: no list decides.
|
|
if d := m.Decide("text/plain"); d.ID != "" || len(d.Skipped) != 1 {
|
|
t.Fatalf("%+v", d)
|
|
}
|
|
// The desktop's own list comes first of all.
|
|
m2 := mimeMachine(t, map[string]string{home + "/.config/i3-mimeapps.list": "[Default Applications]\ninode/directory=thunar.desktop\n"})
|
|
if d := m2.Decide("inode/directory"); d.ID != "thunar.desktop" || d.File != "~/.config/i3-mimeapps.list" {
|
|
t.Fatalf("%+v", d)
|
|
}
|
|
}
|
|
|
|
// recorded answers xdg-mime and gio as they answered on the laptop on 2026-10-05.
|
|
func recorded(calls *[]string) Runner {
|
|
return func(_ context.Context, env []string, name string, args ...string) (string, error) {
|
|
*calls = append(*calls, name+" "+strings.Join(args, " "))
|
|
key := name + " " + strings.Join(args, " ")
|
|
switch key {
|
|
case "xdg-mime query default audio/mpeg":
|
|
return "audacity.desktop\n", nil
|
|
case "gio mime audio/mpeg":
|
|
return "Default application for “audio/mpeg”: mpv.desktop\nRegistered applications:\n\tmpv.desktop\n\taudacity.desktop\nRecommended applications:\n\tmpv.desktop\n\taudacity.desktop\n", nil
|
|
case "xdg-mime query default x-scheme-handler/https":
|
|
return "chromium.desktop\n", nil
|
|
case "gio mime x-scheme-handler/https":
|
|
return "Default application for “x-scheme-handler/https”: chromium.desktop\nRegistered applications:\n\tchromium.desktop\n", nil
|
|
case "xdg-mime query default x-scheme-handler/nothing-here":
|
|
return "", nil
|
|
case "gio mime x-scheme-handler/nothing-here":
|
|
return "No default applications for “x-scheme-handler/nothing-here”\n", nil
|
|
}
|
|
return "", nil
|
|
}
|
|
}
|
|
|
|
func TestTwoReadersThatDisagreeAreSaid(t *testing.T) {
|
|
var calls []string
|
|
m := mimeMachine(t, map[string]string{
|
|
"/usr/share/applications/audacity.desktop": app("Audacity", "audacity %F", "audio/mpeg;"),
|
|
"/usr/share/applications/mpv.desktop": app("mpv", "mpv -- %U", "audio/mpeg;video/mp4;"),
|
|
"/usr/bin/audacity": "#!/bin/sh\n",
|
|
})
|
|
m.Run = recorded(&calls)
|
|
h := m.Handle("audio/mpeg")
|
|
if h.XdgOpen != "audacity.desktop" || h.Gio != "mpv.desktop" || !h.Disagree || h.App == nil || !h.App.Found {
|
|
t.Fatalf("%+v", h)
|
|
}
|
|
if !strings.Contains(h.Decision.File, "no list names") {
|
|
t.Fatalf("%+v", h.Decision)
|
|
}
|
|
none := m.Handle("x-scheme-handler/nothing-here")
|
|
if none.XdgOpen != "" || none.Gio != "" || none.Problem != "nothing opens it" {
|
|
t.Fatalf("%+v", none)
|
|
}
|
|
}
|
|
|
|
func TestTheEnvironmentGivenIsTheAccountsXdgOnly(t *testing.T) {
|
|
var env []string
|
|
m := mimeMachine(t, nil)
|
|
m.Run = func(_ context.Context, e []string, _ string, _ ...string) (string, error) { env = e; return "", nil }
|
|
m.Handle("text/html")
|
|
joined := strings.Join(env, "\n")
|
|
for _, want := range []string{"HOME=" + home, "XDG_CONFIG_HOME=" + home + "/.config", "XDG_CURRENT_DESKTOP=i3", "LANG=C.UTF-8"} {
|
|
if !strings.Contains(joined, want) {
|
|
t.Errorf("lacks %s: %v", want, env)
|
|
}
|
|
}
|
|
if len(env) != 7 {
|
|
t.Errorf("only the seven XDG words are passed on: %v", env)
|
|
}
|
|
}
|
|
|
|
func TestSettingADefaultWritesThePersonsListThroughXdgMime(t *testing.T) {
|
|
var calls []string
|
|
m := mimeMachine(t, nil)
|
|
m.Run = recorded(&calls)
|
|
a, err := m.SetDefault("x-scheme-handler/https", "chromium.desktop")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !contains(calls, "xdg-mime default chromium.desktop x-scheme-handler/https") {
|
|
t.Fatalf("%v", calls)
|
|
}
|
|
if a["written_to"] != "~/.config/mimeapps.list" || a["after"] != "chromium.desktop" || a["problem"] != nil {
|
|
t.Fatalf("%v", a)
|
|
}
|
|
for _, refused := range []string{"gone.desktop", "../../etc/passwd", "firefox", "x.desktop; rm"} {
|
|
calls = nil
|
|
if _, err := m.SetDefault("text/html", refused); err == nil || len(calls) != 0 {
|
|
t.Errorf("%s: %v %v", refused, err, calls)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDeadDefaultsBrokenHandlersAndListsAreFound(t *testing.T) {
|
|
m := mimeMachine(t, map[string]string{
|
|
home + "/.local/share/applications/mimeapps.list": "->/home/op/.retired/dotfiles/mimeapps.list",
|
|
home + "/.local/share/applications/defaults.list": "->/home/op/.retired/dotfiles/mimeapps.list",
|
|
home + "/.local/share/applications/fleet-old.desktop": app("Fleet", "/home/op/gone/Fleet %u", "x-scheme-handler/fleet;"),
|
|
})
|
|
dead := m.deadDefaults()
|
|
var ids []string
|
|
for _, d := range dead {
|
|
// The fake installs none of the mesh's list's applications but firefox and thunar.
|
|
if d.File == "~/.config/mimeapps.list" {
|
|
ids = append(ids, d.Type+"="+d.ID)
|
|
}
|
|
}
|
|
if !reflect.DeepEqual(ids, []string{"text/plain=code-oss.desktop", "x-scheme-handler/https=gone.desktop"}) {
|
|
t.Fatalf("%v", ids)
|
|
}
|
|
var broken []string
|
|
for _, e := range m.brokenHandlers() {
|
|
broken = append(broken, e.ID)
|
|
}
|
|
// xplr's program is not installed in the fake, nor fleet's.
|
|
if !reflect.DeepEqual(broken, []string{"fleet-old.desktop", "fleet.desktop", "xplr.desktop"}) {
|
|
t.Fatalf("%v", broken)
|
|
}
|
|
c := m.Check("i3")
|
|
checks := map[string]Check{}
|
|
for _, x := range c["checks"].([]Check) {
|
|
checks[x.Name] = x
|
|
}
|
|
if s := checks["the mimeapps lists are sane"]; s.OK || !strings.Contains(s.Detail, "a link to nothing") || !strings.Contains(s.Detail, "defaults.list") {
|
|
t.Fatalf("%+v", s)
|
|
}
|
|
if s := checks["the mesh's default applications are in place"]; !s.OK {
|
|
t.Fatalf("%+v", s)
|
|
}
|
|
}
|
|
|
|
func TestAMeshBlockInThePersonsListIsWhatTheCheckRefuses(t *testing.T) {
|
|
// Why the module does not write a block into ~/.config/mimeapps.list: GLib reads the last of two
|
|
// groups, xdg-mime the first, and either rewrites the file around the block.
|
|
m := mimeMachine(t, map[string]string{
|
|
home + "/.config/mimeapps.list": "# >>> mesh >>>\n[Default Applications]\ntext/html=firefox.desktop\n# <<< mesh <<<\n[Default Applications]\ntext/html=chromium.desktop\n",
|
|
})
|
|
c := m.Check("i3")
|
|
for _, x := range c["checks"].([]Check) {
|
|
if x.Name == "the mimeapps lists are sane" && (x.OK || !strings.Contains(x.Detail, "written twice")) {
|
|
t.Fatalf("%+v", x)
|
|
}
|
|
}
|
|
}
|
|
|
|
func autostartMachine(t *testing.T) *Machine {
|
|
return fake(t, map[string]string{
|
|
home + "/.config/autostart/slack.desktop": app("Slack", "/usr/bin/slack -s %U", "") + "Hidden=false\n",
|
|
home + "/.config/autostart/blueman.desktop": app("Blueman", "blueman-applet", "") + "Hidden=true\n",
|
|
home + "/.config/autostart/nextcloud.desktop": app("Nextcloud", `"/usr/bin/nextcloud" --background`, "") + "X-GNOME-Autostart-enabled=false\n",
|
|
"/etc/xdg/autostart/blueman.desktop": app("Blueman", "blueman-applet", ""),
|
|
"/etc/xdg/autostart/picom.desktop": app("picom", "picom", "") + "TryExec=picom\n",
|
|
"/etc/xdg/autostart/nm-applet.desktop": app("nm", "nm-applet", "") + "NotShowIn=KDE;GNOME;\n",
|
|
"/etc/xdg/autostart/gnome-keyring.desktop": app("keyring", "/usr/bin/gnome-keyring-daemon --start", "") + "OnlyShowIn=GNOME;Unity;\n",
|
|
"/etc/xdg/autostart/geoclue.desktop": app("geoclue", "/usr/lib/geoclue-2.0/demos/agent", "") + "NotShowIn=i3;\n",
|
|
"/etc/xdg/autostart/print.desktop": app("print", "system-config-printer-applet", "") + "TryExec=system-config-printer-applet\n",
|
|
"/etc/xdg/autostart/polychromatic.desktop": app("poly", "polychromatic-helper --autostart", ""),
|
|
"/etc/xdg/autostart/slack-too.desktop": app("Slack", "/usr/bin/slack", ""),
|
|
home + "/.config/i3/config": "exec --no-startup-id dex --autostart --environment i3\nexec --no-startup-id picom\n",
|
|
home + "/.config/i3/config.d/90-x.conf": "exec_always --no-startup-id polychromatic-tray-applet\n",
|
|
"/usr/bin/slack": "#!/bin/sh\n",
|
|
"/usr/bin/picom": "#!/bin/sh\n",
|
|
"/usr/bin/nm-applet": "#!/bin/sh\n",
|
|
"/usr/bin/nextcloud": "#!/bin/sh\n",
|
|
"/usr/bin/polychromatic-helper": "#!/bin/sh\n",
|
|
"/proc/101/cmdline": "/usr/lib/slack/slack --type=renderer --enable-crash-reporter",
|
|
"/proc/101/comm": "slack\n",
|
|
"/proc/102/cmdline": "picom\x00",
|
|
"/proc/103/cmdline": "python3\x00/usr/bin/polychromatic-helper\x00--autostart\x00",
|
|
"/proc/104/cmdline": "nextcloud-sync-helper\x00",
|
|
"/proc/self/cmdline": "not a process\x00",
|
|
})
|
|
}
|
|
|
|
func TestAutostartIsDecidedAsDexDecides(t *testing.T) {
|
|
m := autostartMachine(t)
|
|
got := map[string]Autostart{}
|
|
for _, a := range m.Autostarts("i3") {
|
|
got[a.Scope+"/"+a.ID] = a
|
|
}
|
|
want := map[string]bool{
|
|
"user/slack.desktop": true, "user/blueman.desktop": false, "user/nextcloud.desktop": true,
|
|
"system/blueman.desktop": false, "system/picom.desktop": true, "system/nm-applet.desktop": true,
|
|
"system/gnome-keyring.desktop": false, "system/geoclue.desktop": false, "system/print.desktop": false,
|
|
"system/polychromatic.desktop": true, "system/slack-too.desktop": true,
|
|
}
|
|
if len(got) != len(want) {
|
|
t.Fatalf("%d entries: %v", len(got), got)
|
|
}
|
|
for k, starts := range want {
|
|
if got[k].DexStarts != starts {
|
|
t.Errorf("%s: dex starts %v (%s)", k, got[k].DexStarts, got[k].Why)
|
|
}
|
|
}
|
|
if a := got["system/blueman.desktop"]; a.OverriddenBy != "~/.config/autostart/blueman.desktop" || got["user/blueman.desktop"].Overrides != "/etc/xdg/autostart/blueman.desktop" {
|
|
t.Errorf("the user's entry hides the system's of the same name: %+v", a)
|
|
}
|
|
if a := got["user/nextcloud.desktop"]; !strings.Contains(a.Why, "does not stop dex") {
|
|
t.Errorf("%s", a.Why)
|
|
}
|
|
if a := got["system/print.desktop"]; !strings.Contains(a.Why, "TryExec") {
|
|
t.Errorf("%s", a.Why)
|
|
}
|
|
if a := got["system/picom.desktop"]; a.AlsoStarted != "~/.config/i3/config:2" || a.Running != 1 {
|
|
t.Errorf("picom: %+v", a)
|
|
}
|
|
if got["user/slack.desktop"].Running != 1 || got["system/polychromatic.desktop"].Running != 1 || got["user/nextcloud.desktop"].Running != 0 {
|
|
t.Errorf("running: slack %d, polychromatic %d, nextcloud %d", got["user/slack.desktop"].Running,
|
|
got["system/polychromatic.desktop"].Running, got["user/nextcloud.desktop"].Running)
|
|
}
|
|
}
|
|
|
|
func TestTheCheckFindsTwoStartersAndAMissingProgram(t *testing.T) {
|
|
m := autostartMachine(t)
|
|
checks := map[string]Check{}
|
|
for _, c := range m.Check("i3")["checks"].([]Check) {
|
|
checks[c.Name] = c
|
|
}
|
|
twice := checks["no program is started twice at login"]
|
|
if twice.OK || !strings.Contains(twice.Detail, "picom: by /etc/xdg/autostart/picom.desktop and by ~/.config/i3/config:2") ||
|
|
!strings.Contains(twice.Detail, "slack: by ~/.config/autostart/slack.desktop and /etc/xdg/autostart/slack-too.desktop") {
|
|
t.Fatalf("%+v", twice)
|
|
}
|
|
// blueman's system entry is hidden by the user's, so dex starts nothing that is missing.
|
|
if c := checks["every program dex starts is installed"]; !c.OK {
|
|
t.Fatalf("%+v", c)
|
|
}
|
|
if c := checks["user-dirs.dirs is the mesh's"]; c.OK {
|
|
t.Fatalf("no file, yet %+v", c)
|
|
}
|
|
}
|