openrazer (the official driver, daemon and library), polychromatic (the AUR tray, kept as found; its i3 line moves out of the i3 module into its own node-display-session contribution), forticlient (the AUR VPN client: its service declared, its configuration never read) and nm-applet (the desktop half of NetworkManager, apart from the server-side module). The openrazer daemon fails on both workstations because the account is not in the openrazer group; openrazer_check names it and the README carries the one-off step, since the account's user resource is zsh's. desktop.go learns to tell a program from another sharing its 15-character command name, so polychromatic's tools never count or end themselves, and a copies test holds the six carriers to one text.
220 lines
8.0 KiB
Go
220 lines
8.0 KiB
Go
package main
|
|
|
|
// The fake machine the tests run against, and the tests of desktop.go. The same in every desktop
|
|
// application's bundle (copies_test.go).
|
|
|
|
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 TestAProgramIsToldFromAnotherSharingItsCutName(t *testing.T) {
|
|
f := newFake(t)
|
|
f.proc(10, 1000, "polychromatic-t", []string{"/usr/bin/python", "/usr/bin/polychromatic-tray-applet"}, "s.scope")
|
|
f.proc(11, 1000, "polychromatic-t", []string{"polychromatic-tray-applet"}, "s.scope")
|
|
f.proc(12, 1000, "polychromatic-t", []string{"/usr/lib/mesh/polychromatic-tools"}, "s.scope")
|
|
if got := f.procsOf("polychromatic-t", "polychromatic-tray-applet"); len(got) != 2 || got[0].PID != 10 || got[1].PID != 11 {
|
|
t.Fatalf("%+v", got)
|
|
}
|
|
if got := f.procsOf("polychromatic-t", ""); len(got) != 3 {
|
|
t.Fatalf("%+v", got)
|
|
}
|
|
ended, _ := f.stopProcs(time.Second, f.procsOf("polychromatic-t", "polychromatic-tray-applet"))
|
|
if len(ended) != 2 || len(f.procs("polychromatic-t")) != 1 {
|
|
t.Fatalf("ended %v; the tools' own process must stay", ended)
|
|
}
|
|
}
|
|
|
|
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))
|
|
}
|
|
}
|