package main // The fake machine the tests run against, and the tests of desktop.go. The same in the // nextcloud-client and blueman bundles. import ( "context" "os" "path/filepath" "strconv" "strings" "sync" "syscall" "testing" "time" ) const testHome = "/home/operator" // fake is a machine with a fake root, a scripted Runner and signals that end fake processes. type fake struct { *Machine t *testing.T mu sync.Mutex calls []string answer func(name string, args []string) Output // onStart is run when systemd-run starts something, to let a fake process appear. onStart func(argv []string) // stubborn pids ignore SIGTERM. stubborn map[int]bool signals []string } func newFake(t *testing.T) *fake { t.Helper() root := t.TempDir() f := &fake{t: t, stubborn: map[int]bool{}} f.Machine = &Machine{Root: root, Home: testHome, UID: 1000, Timeout: CallTimeout, Sleep: func(time.Duration) {}, Now: func() time.Time { return time.Unix(1_800_000_000, 0) }} f.Run = func(_ context.Context, env []string, name string, args ...string) Output { f.mu.Lock() f.calls = append(f.calls, strings.TrimSpace(name+" "+strings.Join(args, " "))) f.mu.Unlock() if name == "systemd-run" && f.onStart != nil { for i, a := range args { if a == "--" { f.onStart(args[i+1:]) } } } if f.answer != nil { return f.answer(name, args) } return Output{} } f.Kill = func(pid int, sig syscall.Signal) error { f.signals = append(f.signals, strconv.Itoa(pid)+":"+sig.String()) if sig == syscall.SIGKILL || !f.stubborn[pid] { return os.RemoveAll(filepath.Join(root, "proc", strconv.Itoa(pid))) } return nil } f.write("/proc/stat", "cpu 1 2 3\nbtime 1799990000\n") return f } func (f *fake) write(path, content string) { f.t.Helper() p := filepath.Join(f.Root, path) if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil { f.t.Fatal(err) } if err := os.WriteFile(p, []byte(content), 0o644); err != nil { f.t.Fatal(err) } } // proc adds a process of uid with a command name, argv, cgroup and environment. func (f *fake) proc(pid, uid int, comm string, argv []string, cgroup string, env ...string) { d := "/proc/" + strconv.Itoa(pid) + "/" f.write(d+"comm", comm+"\n") f.write(d+"status", "Name:\t"+comm+"\nUid:\t"+strconv.Itoa(uid)+"\t"+strconv.Itoa(uid)+"\t"+strconv.Itoa(uid)+"\t"+strconv.Itoa(uid)+"\n") f.write(d+"cmdline", strings.Join(argv, "\x00")+"\x00") f.write(d+"cgroup", "0::/user.slice/user-"+strconv.Itoa(uid)+".slice/"+cgroup+"\n") f.write(d+"environ", strings.Join(env, "\x00")+"\x00") // starttime (field 22) is 1000 ticks: 10 s after boot. f.write(d+"stat", strconv.Itoa(pid)+" ("+comm+") S 1 1 1 0 -1 0 0 0 0 0 0 0 0 0 20 0 1 0 1000 0 0\n") } func (f *fake) desktopSession() { f.proc(3700, 1000, "i3", []string{"i3"}, "session-c1.scope", "DISPLAY=:1", "XAUTHORITY="+testHome+"/.Xauthority") f.write("/run/user/1000/bus", "") } func (f *fake) called(prefix string) bool { for _, c := range f.calls { if strings.HasPrefix(c, prefix) { return true } } return false } func TestProcessesAreTheAccountsOwnWithWhereAndWhenTheyStarted(t *testing.T) { f := newFake(t) f.proc(10, 1000, "worker", []string{"/usr/bin/worker", "--background"}, "session-c1.scope") f.proc(11, 1001, "worker", []string{"/usr/bin/worker"}, "session-c2.scope") f.proc(12, 1000, "other", []string{"other"}, "x.scope") got := f.procs("worker") if len(got) != 1 || got[0].PID != 10 || got[0].Command != "/usr/bin/worker --background" || got[0].StartedIn != "session-c1.scope" || got[0].Since != time.Unix(1799990010, 0).UTC().Format(time.RFC3339) { t.Fatalf("%+v", got) } } func TestTheSessionIsTheWindowManagersAndNoneIsSaidPlainly(t *testing.T) { f := newFake(t) if _, err := f.session(); err == nil || !strings.Contains(err.Error(), "no graphical session") { t.Fatalf("%v", err) } f.proc(50, 1000, "xterm", []string{"xterm"}, "s.scope", "DISPLAY=:9") f.desktopSession() f.proc(60, 1001, "i3", []string{"i3"}, "s.scope", "DISPLAY=:5") s, err := f.session() if err != nil || s.Display != ":1" || s.XAuthority != testHome+"/.Xauthority" || s.Bus != "unix:path=/run/user/1000/bus" || !strings.Contains(s.From, "i3") { t.Fatalf("%+v %v", s, err) } } func TestStopAsksThenForcesAndDetachStartsUnderTheServiceManager(t *testing.T) { f := newFake(t) f.desktopSession() f.proc(20, 1000, "app", []string{"app"}, "s.scope") f.proc(21, 1000, "app", []string{"app"}, "s.scope") f.stubborn[21] = true ended, killed := f.stop(time.Second, "app") if len(ended) != 1 || ended[0] != 20 || len(killed) != 1 || killed[0] != 21 { t.Fatalf("ended %v killed %v (%v)", ended, killed, f.signals) } s, _ := f.session() if err := f.detach(s, "mesh-app", "/usr/bin/app", "--background"); err != nil { t.Fatal(err) } want := "systemd-run --user --collect --quiet --unit=mesh-app --setenv=DISPLAY=:1 --setenv=XAUTHORITY=" + testHome + "/.Xauthority -- /usr/bin/app --background" if !f.called("systemctl --user stop mesh-app.service") || !f.called(want) { t.Fatalf("%q", f.calls) } } func TestAnAutostartEntryOfTheAccountOverridesTheSystemsAndHiddenStartsNothing(t *testing.T) { f := newFake(t) if a := f.autostart("x.desktop"); a.Starts || a.Because == "" { t.Fatalf("%+v", a) } f.write("/etc/xdg/autostart/x.desktop", "[Desktop Entry]\nExec=x-applet\n[Desktop Action y]\nExec=other\n") if a := f.autostart("x.desktop"); !a.Starts || a.Exec != "x-applet" || a.From != "/etc/xdg/autostart/x.desktop" { t.Fatalf("%+v", a) } f.write(testHome+"/.config/autostart/x.desktop", "[Desktop Entry]\nExec=x-applet\nHidden=true\n") if a := f.autostart("x.desktop"); a.Starts || a.Because != "Hidden=true" || a.From != "~/.config/autostart/x.desktop" { t.Fatalf("%+v", a) } } func TestAWindowManagerStartIsFoundInTheConfigurationAndItsDropIns(t *testing.T) { f := newFake(t) f.write(testHome+"/.config/i3/config", "exec --no-startup-id dex --autostart --environment i3\n# exec app\nbindsym $mod+a exec app\n") f.write(testHome+"/.config/i3/config.d/50-x.conf", "exec_always --no-startup-id /usr/bin/app --flag\n") got := f.i3Starts("app") if len(got) != 1 || got[0] != "~/.config/i3/config.d/50-x.conf:1: exec_always --no-startup-id /usr/bin/app --flag" { t.Fatalf("%q", got) } } func TestACommandThatFailsIsNamed(t *testing.T) { if err := failed(Output{Code: 127, Err: ErrNotInstalled}, "dex"); err == nil || !strings.Contains(err.Error(), "dex is not installed") { t.Fatal(err) } if err := failed(Output{Code: 1, Stderr: "nope"}, "pacman", "-Q", "x"); err == nil || !strings.Contains(err.Error(), "pacman -Q x exited 1: nope") { t.Fatal(err) } if err := failed(Output{}, "true"); err != nil { t.Fatal(err) } } func TestTheRealRunnerBoundsTimeAndOutput(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) defer cancel() if o := execRun(ctx, nil, "sleep", "5"); o.Err != ErrTimedOut { t.Fatalf("%+v", o) } if o := execRun(context.Background(), nil, "no-such-program-here"); o.Err != ErrNotInstalled { t.Fatalf("%+v", o) } o := execRun(context.Background(), nil, "head", "-c", strconv.Itoa(MostOutput+10), "/dev/zero") if !o.Cut || len(o.Stdout) != MostOutput { t.Fatalf("cut %v, %d bytes", o.Cut, len(o.Stdout)) } }