The distribution's i3lock behind a locker that releases xss-lock's sleep lock once it is up; timeouts and xss-lock from the session's xinitrc slot, ending with the session; i3lock-color and xscreensaver declared absent; Go tools lock, idle, inhibit and locked.
190 lines
6.8 KiB
Go
190 lines
6.8 KiB
Go
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)
|
|
}
|
|
}
|