package main import ( "errors" "os" "os/exec" "path/filepath" "strconv" "strings" "testing" "time" ) const nobody = 4194400 // xset q as the laptop answered it on 2026-10-04 (keyboard lines shortened). const xsetQOutput = `Keyboard Control: auto repeat: on key click percent: 0 LED mask: 00000000 Screen Saver: prefer blanking: yes allow exposures: yes timeout: 600 cycle: 600 Colors: default colormap: 0x20 BlackPixel: 0x0 WhitePixel: 0xffffff DPMS (Display Power Management Signaling): Standby: 1800 Suspend: 1800 Off: 3600 DPMS is Enabled Monitor is On ` func TestTheTimeoutsAreReadFromXset(t *testing.T) { st, err := parseXsetQ(xsetQOutput) if err != nil { t.Fatal(err) } if st.LockAfterSeconds != 600 || st.CycleSeconds != 600 || !st.DPMSEnabled || st.StandbySeconds != 1800 || st.SuspendSeconds != 1800 || st.OffSeconds != 3600 || !st.MonitorOn { t.Fatalf("%+v", st) } if _, err := parseXsetQ("nothing"); err == nil { t.Fatal("an answer without a screensaver was accepted") } } func TestAProcessStartsWhenItsStatAndTheBootTimeSay(t *testing.T) { fakeMachine(t) fakeProcess(t, nobody, "i3lock") // A command name with a space and a parenthesis, which a naive split gets wrong. stat := strconv.Itoa(nobody) + " (i3 lock) x) S 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 12345 0 0\n" if err := os.WriteFile(filepath.Join(procRoot, strconv.Itoa(nobody), "stat"), []byte(stat), 0o644); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(procRoot, "stat"), []byte("cpu 1 2 3\nbtime 1700000000\n"), 0o644); err != nil { t.Fatal(err) } at, ok := startTime(nobody) if !ok || !at.Equal(time.Unix(1700000000, 0).Add(123450*time.Millisecond)) { t.Fatalf("%v %v", at, ok) } } // lockingMachine is a session whose loginctl, asked to lock, starts a (fake) i3lock. func lockingMachine(t *testing.T, watcher bool) string { t.Helper() fakeMachine(t) fakeProcess(t, nobody, "i3", "DISPLAY=:1", "XDG_SESSION_ID=4") if watcher { fakeProcess(t, nobody+1, "xss-lock", "DISPLAY=:1") } if err := os.MkdirAll(filepath.Join(runUserDir, strconv.Itoa(os.Getuid())), 0o700); err != nil { t.Fatal(err) } lockNow := `mkdir -p "$PROC/` + strconv.Itoa(nobody+2) + `" && echo i3lock > "$PROC/` + strconv.Itoa(nobody+2) + `/comm"` bin := fakeBinaries(t, map[string]string{ "loginctl": `echo "loginctl $*" >> "$LOG" case "$1" in lock-session) ` + lockNow + ` ;; show-session) echo yes ;; esac`, "systemd-run": `echo "systemd-run $*" >> "$LOG"; ` + lockNow, "systemctl": `echo "systemctl $*" >> "$LOG"; echo inactive`, }) t.Setenv("LOG", filepath.Join(bin, "log")) t.Setenv("PROC", procRoot) return bin } func TestLockGoesThroughLogindSoTheOneLockerAnswers(t *testing.T) { bin := lockingMachine(t, true) st, err := Lock() if err != nil { t.Fatal(err) } if !st.Locked || !st.Watcher || st.LockedHint == nil || !*st.LockedHint || !strings.Contains(st.Via, "logind") { t.Fatalf("%+v", st) } log, _ := os.ReadFile(filepath.Join(bin, "log")) if !strings.Contains(string(log), "loginctl lock-session 4\n") || strings.Contains(string(log), "systemd-run") { t.Fatalf("asked:\n%s", log) } again, err := Lock() if err != nil || again.Via != "already locked" { t.Fatalf("locking a locked screen: %+v, %v", again, err) } } func TestWithoutTheWatcherTheLockerRunsUnderTheAccountsServiceManager(t *testing.T) { bin := lockingMachine(t, false) st, err := Lock() if err != nil || !st.Locked || !strings.Contains(st.Via, "xss-lock is not running") { t.Fatalf("%+v, %v", st, err) } log, _ := os.ReadFile(filepath.Join(bin, "log")) if !strings.Contains(string(log), "--unit=screen-lock --setenv=DISPLAY=:1 --setenv=XDG_SESSION_ID=4 -- ") || !strings.Contains(string(log), "/.local/bin/screen-lock") { t.Fatalf("asked:\n%s", log) } } func TestLockedWithoutASessionIsAnAnswerNotAnError(t *testing.T) { fakeMachine(t) st, err := Locked() if err != nil || st.Locked || st.Watcher || st.PIDs == nil { t.Fatalf("%+v, %v", st, err) } if _, err := Lock(); !errors.Is(err, ErrNoSession) { t.Fatalf("lock without a session: %v", err) } } func TestIdleChangesOnlyWhatWasAskedAndSaysForHowLong(t *testing.T) { fakeMachine(t) fakeProcess(t, nobody, "i3", "DISPLAY=:1") bin := fakeBinaries(t, map[string]string{"xset": `echo "xset $*" >> "$LOG"; [ "$1" = q ] && cat "$Q"; true`}) t.Setenv("LOG", filepath.Join(bin, "log")) q := filepath.Join(bin, "q") if err := os.WriteFile(q, []byte(xsetQOutput), 0o644); err != nil { t.Fatal(err) } t.Setenv("Q", q) if st, err := Idle(IdleChange{}); err != nil || st.Note != "" || st.LockAfterSeconds != 600 { t.Fatalf("read: %+v, %v", st, err) } c, err := idleChangeOf(map[string]any{"lock_after_seconds": float64(900), "off_seconds": float64(7200)}) if err != nil { t.Fatal(err) } st, err := Idle(c) if err != nil || !strings.Contains(st.Note, "next login") { t.Fatalf("%+v, %v", st, err) } log, _ := os.ReadFile(filepath.Join(bin, "log")) if !strings.Contains(string(log), "xset s 900 600\n") || !strings.Contains(string(log), "xset dpms 1800 1800 7200\n") { t.Fatalf("asked:\n%s", log) } if _, err := idleChangeOf(map[string]any{"off_seconds": float64(-1)}); err == nil { t.Fatal("a negative timeout was accepted") } } func TestAnInhibitionTurnsIdleOffAndRestoresWhatWasThereWhenItEndsOrIsStopped(t *testing.T) { was := IdleState{LockAfterSeconds: 1800, CycleSeconds: 1800, DPMSEnabled: true, StandbySeconds: 1800, SuspendSeconds: 1800, OffSeconds: 3600} script := inhibitScript(2, was) if !strings.Contains(script, "xset s off -dpms; sleep 120 & wait; restore") || !strings.Contains(script, "restore() { xset s 1800 1800; xset dpms 1800 1800 3600; xset +dpms; }") || !strings.Contains(script, "trap 'restore; exit 0' TERM") { t.Fatalf("%s", script) } // Run it for real with a fake xset, stopped early as systemctl stop would. dir := t.TempDir() bin := fakeBinaries(t, map[string]string{"xset": `echo "$*" >> "` + filepath.Join(dir, "log") + `"`}) _ = bin cmd := exec.Command("/bin/sh", "-c", inhibitScript(1, was)) if err := cmd.Start(); err != nil { t.Fatal(err) } time.Sleep(300 * time.Millisecond) _ = cmd.Process.Signal(os.Interrupt) _ = cmd.Wait() got, _ := os.ReadFile(filepath.Join(dir, "log")) if string(got) != "s off -dpms\ns 1800 1800\ndpms 1800 1800 3600\n+dpms\n" { t.Fatalf("the timeouts were not restored on stop:\n%s", got) } } func TestTheDeclaredTimeoutsAreTheSessionStartsOwn(t *testing.T) { m := readManifest(t) code := m.Shell[0].Code if !strings.Contains(code, "xset s "+strconv.Itoa(declaredLock)+" "+strconv.Itoa(declaredLock)+"\n") || !strings.Contains(code, "xset dpms "+strconv.Itoa(declaredStandby)+" "+strconv.Itoa(declaredSuspend)+" "+strconv.Itoa(declaredOff)+"\n") { t.Fatalf("the tools' declared values and the session start's disagree:\n%s", code) } }