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.
186 lines
7.4 KiB
Go
186 lines
7.4 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
const serial = "PM2148H00000001"
|
|
|
|
// newStack is a machine with the stack as it should be: built and loaded, a mouse bound on three
|
|
// interfaces, the account in the group (and its service manager with it), the daemon running from its
|
|
// unit and answering. member and running take parts of it away.
|
|
func newStack(t *testing.T, member, running bool) *fake {
|
|
f := newFake(t)
|
|
f.write("/proc/sys/kernel/osrelease", "7.2.8-arch1-2\n")
|
|
f.write("/sys/module/razermouse/refcnt", "0\n")
|
|
for _, i := range []string{"0004", "0005", "0006"} {
|
|
f.write("/sys/bus/hid/drivers/razermouse/0003:1532:00AB."+i+"/device_type", "")
|
|
}
|
|
f.write("/sys/bus/hid/drivers/razermouse/bind", "")
|
|
f.write("/etc/passwd", "root:x:0:0::/root:/bin/sh\noperator:x:1000:1000::/home/operator:/bin/zsh\n")
|
|
members := "operator"
|
|
if !member {
|
|
members = ""
|
|
}
|
|
f.write("/etc/group", "plugdev:x:970:operator\nopenrazer:x:958:"+members+"\n")
|
|
f.write(activation, "[D-BUS Service]\nName=org.razer\nSystemdService=openrazer-daemon.service\n")
|
|
f.proc(3596, 1000, "systemd", []string{"/usr/lib/systemd/systemd", "--user"}, "user@1000.service/init.scope")
|
|
groups := "970 958"
|
|
if !member {
|
|
groups = "970"
|
|
}
|
|
f.write("/proc/3596/status", "Name:\tsystemd\nUid:\t1000\t1000\t1000\t1000\nGroups:\t"+groups+"\n")
|
|
if running {
|
|
f.proc(4269, 1000, daemonComm, []string{"openrazer-daemon", "-F"}, "user@1000.service/app.slice/"+daemonUnit)
|
|
}
|
|
f.answer = func(name string, args []string) Output {
|
|
call := name + " " + strings.Join(args, " ")
|
|
switch {
|
|
case name == "pacman":
|
|
return Output{Stdout: args[1] + " 3.12.4-1\n"}
|
|
case name == "dkms":
|
|
return Output{Stdout: "openrazer-driver/3.12.4, 7.2.8-arch1-2, x86_64: installed\n"}
|
|
case name == "systemctl" && strings.Contains(call, "is-active"):
|
|
if running {
|
|
return Output{Stdout: "active\n"}
|
|
}
|
|
return Output{Stdout: "failed\n", Code: 3}
|
|
case name == "systemctl" && strings.Contains(call, "is-enabled"):
|
|
return Output{Stdout: "disabled\n", Code: 1}
|
|
case name == "journalctl":
|
|
return Output{Stdout: "Starting daemon.\n2026-10-04 16:26:21 | razer | CRITICAL | User is not a member of the openrazer group\n" +
|
|
"Traceback (most recent call last):\n2026-10-04 16:26:22 | razer | CRITICAL | User is not a member of the openrazer group\nSystemExit: 0\n"}
|
|
case name == "busctl" && !running:
|
|
return Output{Code: 1, Stderr: "Call failed: The name is not activatable"}
|
|
case strings.HasSuffix(call, " version"):
|
|
return Output{Stdout: `{"type":"s","data":["3.12.4"]}`}
|
|
case strings.HasSuffix(call, " getDevices"):
|
|
return Output{Stdout: `{"type":"as","data":[["` + serial + `"]]}`}
|
|
case strings.HasSuffix(call, " getDeviceName"):
|
|
return Output{Stdout: `{"type":"s","data":["Razer Basilisk V3 Pro"]}`}
|
|
case strings.HasSuffix(call, " getDeviceType"):
|
|
return Output{Stdout: `{"type":"s","data":["mouse"]}`}
|
|
case strings.HasSuffix(call, " getFirmware"):
|
|
return Output{Stdout: `{"type":"s","data":["v1.04"]}`}
|
|
case strings.HasSuffix(call, " getBattery"):
|
|
return Output{Stdout: `{"type":"d","data":[85.0]}`}
|
|
case strings.HasSuffix(call, " isCharging"):
|
|
return Output{Code: 1, Stderr: "Unknown method"}
|
|
}
|
|
return Output{}
|
|
}
|
|
return f
|
|
}
|
|
|
|
func TestStatusReadsTheDriverTheGroupAndTheDaemonWithoutStartingIt(t *testing.T) {
|
|
f := newStack(t, true, true)
|
|
s, err := f.Status()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if s.Installed["openrazer-daemon"] != "3.12.4-1" || s.Driver.Built != "installed" || strings.Join(s.Driver.Loaded, ",") != "razermouse" ||
|
|
len(s.Driver.Bound) != 1 || s.Driver.Bound[0] != (Bound{USB: "1532:00ab", Driver: "razermouse", Interfaces: 3}) {
|
|
t.Fatalf("%+v", s)
|
|
}
|
|
if !s.Group.Exists || !s.Group.Account || s.Group.Manager == nil || !*s.Group.Manager {
|
|
t.Fatalf("%+v", s.Group)
|
|
}
|
|
if s.Daemon.Version != "3.12.4" || len(s.Daemon.Running) != 1 || len(s.Devices) != 1 {
|
|
t.Fatalf("%+v %+v", s.Daemon, s.Devices)
|
|
}
|
|
d := s.Devices[0]
|
|
if d.Name != "Razer Basilisk V3 Pro" || d.Type != "mouse" || d.Battery == nil || *d.Battery != 85 || d.Charging != nil {
|
|
t.Fatalf("%+v", d)
|
|
}
|
|
if len(s.Notes) != 1 || !strings.Contains(s.Notes[0], "plugdev") {
|
|
t.Fatalf("%v", s.Notes)
|
|
}
|
|
for _, c := range f.calls {
|
|
if strings.HasPrefix(c, "busctl") && !strings.Contains(c, "--auto-start=no") {
|
|
t.Fatalf("a bus call that could start the daemon: %s", c)
|
|
}
|
|
}
|
|
raw, _ := json.Marshal(s)
|
|
if strings.Contains(string(raw), serial) {
|
|
t.Fatal("the answer carries a device's serial")
|
|
}
|
|
|
|
stopped := newStack(t, false, false)
|
|
s, _ = stopped.Status()
|
|
if s.Group.Account || s.Group.Manager == nil || *s.Group.Manager || s.Daemon.Active != "failed" || stopped.called("busctl") {
|
|
t.Fatalf("%+v %q", s, stopped.calls)
|
|
}
|
|
if len(s.Daemon.LastWords) != 1 || s.Daemon.LastWords[0] != "razer | CRITICAL | User is not a member of the openrazer group" {
|
|
t.Fatalf("the reason, once, not the traceback: %q", s.Daemon.LastWords)
|
|
}
|
|
}
|
|
|
|
func TestCheckPassesTheWholeStackAndNamesWhatIsMissing(t *testing.T) {
|
|
f := newStack(t, true, true)
|
|
c, err := f.Check()
|
|
if err != nil || !c.OK || len(c.Starts) != 1 || !strings.HasPrefix(c.Starts[0], "D-Bus activation") {
|
|
t.Fatalf("%+v %v", c, err)
|
|
}
|
|
|
|
broken := newStack(t, false, false)
|
|
broken.write("/proc/sys/kernel/osrelease", "7.3.0-arch1-1\n")
|
|
broken.write(testHome+"/.config/i3/config", "exec --no-startup-id openrazer-daemon\n")
|
|
c, _ = broken.Check()
|
|
var all []string
|
|
for _, x := range c.Findings {
|
|
all = append(all, x.What+" => "+x.Do)
|
|
}
|
|
got := strings.Join(all, "\n")
|
|
for _, want := range []string{"not built for the running kernel 7.3.0-arch1-1", "not in the openrazer group", "gpasswd -a $USER openrazer",
|
|
"a second start: ~/.config/i3/config:1", "failed at its last start: razer | CRITICAL | User is not a member"} {
|
|
if !strings.Contains(got, want) {
|
|
t.Errorf("no finding %q in\n%s", want, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAGroupAddedAfterLoginIsNamedAsTheServiceManagers(t *testing.T) {
|
|
f := newStack(t, true, true)
|
|
f.write("/proc/3596/status", "Name:\tsystemd\nUid:\t1000\t1000\t1000\t1000\nGroups:\t970\n")
|
|
c, _ := f.Check()
|
|
if c.OK || len(c.Findings) != 1 || !strings.Contains(c.Findings[0].What, "service manager started before") {
|
|
t.Fatalf("%+v", c)
|
|
}
|
|
}
|
|
|
|
func TestADaemonOutsideItsUnitOrSeeingFewerDevicesIsAFinding(t *testing.T) {
|
|
f := newStack(t, true, true)
|
|
f.write("/proc/4269/cgroup", "0::/user.slice/user-1000.slice/session-c1.scope\n")
|
|
f.write("/sys/bus/hid/drivers/razerkbd/0003:1532:0099.0001/x", "")
|
|
c, _ := f.Check()
|
|
var all []string
|
|
for _, x := range c.Findings {
|
|
all = append(all, x.What)
|
|
}
|
|
got := strings.Join(all, "\n")
|
|
if !strings.Contains(got, "outside its unit (in session-c1.scope)") || !strings.Contains(got, "holds 2 device(s) and the daemon sees 1") {
|
|
t.Fatalf("%s", got)
|
|
}
|
|
}
|
|
|
|
func TestRestartGoesThroughTheUnitAndSaysWhyItFailed(t *testing.T) {
|
|
f := newStack(t, true, true)
|
|
a, err := f.Restart()
|
|
if err != nil || a.Version != "3.12.4" || len(a.Devices) != 1 || !f.called("systemctl --user restart "+daemonUnit) {
|
|
t.Fatalf("%+v %v %q", a, err, f.calls)
|
|
}
|
|
failing := newStack(t, false, false)
|
|
inner := failing.answer
|
|
failing.answer = func(name string, args []string) Output {
|
|
if name == "systemctl" && len(args) > 1 && args[1] == "restart" {
|
|
return Output{Code: 1, Stderr: "Job for openrazer-daemon.service failed"}
|
|
}
|
|
return inner(name, args)
|
|
}
|
|
if _, err := failing.Restart(); err == nil || !strings.Contains(err.Error(), "not a member of the openrazer group") {
|
|
t.Fatalf("%v", err)
|
|
}
|
|
}
|