package wayland import ( "context" "encoding/binary" "io" "net" "os" "slices" "testing" "time" "github.com/bnema/neferafk/internal/ports" "github.com/bnema/zerowrap" "golang.org/x/sys/unix" ) func sockets(t *testing.T) (net.Conn, *net.UnixConn) { t.Helper() fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) if err != nil { t.Fatal(err) } files := []*os.File{os.NewFile(uintptr(fds[0]), "client"), os.NewFile(uintptr(fds[1]), "peer")} a, err := net.FileConn(files[0]) files[0].Close() if err != nil { t.Fatal(err) } b, err := net.FileConn(files[1]) files[1].Close() if err != nil { t.Fatal(err) } t.Cleanup(func() { a.Close(); b.Close() }) return a, b.(*net.UnixConn) } func packet(id uint32, op uint16, body []byte) []byte { b := make([]byte, 8+len(body)) binary.NativeEndian.PutUint32(b, id) binary.NativeEndian.PutUint32(b[4:], uint32(len(b))<<16|uint32(op)) copy(b[8:], body) return b } func words(v ...uint32) []byte { b := make([]byte, 4*len(v)) for i, n := range v { binary.NativeEndian.PutUint32(b[i*4:], n) } return b } func wireString(s string) []byte { b := make([]byte, 4+(len(s)+4)&^3) binary.NativeEndian.PutUint32(b, uint32(len(s)+1)) copy(b[4:], s) return b } func readWire(t *testing.T, p *net.UnixConn) (uint32, uint16, []byte) { t.Helper() p.SetReadDeadline(time.Now().Add(2 * time.Second)) h := make([]byte, 8) if _, err := io.ReadFull(p, h); err != nil { t.Fatal(err) } size := binary.NativeEndian.Uint32(h[4:]) >> 16 b := make([]byte, size-8) if _, err := io.ReadFull(p, b); err != nil { t.Fatal(err) } return binary.NativeEndian.Uint32(h), uint16(binary.NativeEndian.Uint32(h[4:])), b } func sendWire(t *testing.T, p *net.UnixConn, id uint32, op uint16, b []byte) { t.Helper() if _, err := p.Write(packet(id, op, b)); err != nil { t.Fatal(err) } } func global(t *testing.T, p *net.UnixConn, registry, name uint32, iface string, v uint32) { b := append(words(name), wireString(iface)...) b = append(b, words(v)...) sendWire(t, p, registry, 0, b) } func synchronize(t *testing.T, p *net.UnixConn, body []byte) { id := binary.NativeEndian.Uint32(body) sendWire(t, p, id, 0, words(1)) sendWire(t, p, 1, 1, words(id)) } func capability(t *testing.T, out <-chan ports.WaylandEvent) ports.Capabilities { t.Helper() for { select { case e := <-out: if c, ok := e.(ports.WaylandCapabilities); ok { return c.Capabilities } case <-time.After(2 * time.Second): t.Fatal("capability timeout") } } } func nextEvent(t *testing.T, out <-chan ports.WaylandEvent) ports.WaylandEvent { t.Helper() select { case e := <-out: return e case <-time.After(2 * time.Second): t.Fatal("event timeout") return nil } } func TestDiscoveryIdleReloadPowerAndRemoval(t *testing.T) { conn, peer := sockets(t) c, err := New(conn, Options{Requirements: ports.WaylandRequirements{Idle: true, InputWake: true, OutputPower: true, Lock: true, Visual: true}, Generation: 1, Deadlines: []time.Duration{time.Second, time.Second, 2 * time.Second}}, zerowrap.New(zerowrap.Config{Output: io.Discard})) if err != nil { t.Fatal(err) } _, _, body := readWire(t, peer) registry := binary.NativeEndian.Uint32(body) ctx, cancel := context.WithCancel(context.Background()) defer cancel() commands := make(chan ports.WaylandCommand, 8) out := make(chan ports.WaylandEvent, 64) done := make(chan error, 1) go func() { done <- c.Run(ctx, commands, out) }() _, _, body = readWire(t, peer) global(t, peer, registry, 10, "wl_seat", 5) global(t, peer, registry, 11, "wl_output", 4) global(t, peer, registry, 12, "ext_idle_notifier_v1", 2) global(t, peer, registry, 13, "zwlr_output_power_manager_v1", 1) global(t, peer, registry, 14, "ext_session_lock_manager_v1", 1) synchronize(t, peer, body) ids := map[string]uint32{} notifications := map[uint32]uint32{} var wake, power uint32 for { id, op, b := readWire(t, peer) if id == 1 { synchronize(t, peer, b) break } if id == registry { n := int(binary.NativeEndian.Uint32(b[4:])) iface := string(b[8 : 8+n-1]) ids[iface] = binary.NativeEndian.Uint32(b[len(b)-4:]) if iface == "wl_output" { sendWire(t, peer, ids[iface], 4, wireString("HEADLESS-1")) } continue } if id == ids["ext_idle_notifier_v1"] { child := binary.NativeEndian.Uint32(b) ms := binary.NativeEndian.Uint32(b[4:]) if op == 2 { wake = child } else { notifications[ms] = child } } if id == ids["zwlr_output_power_manager_v1"] { power = binary.NativeEndian.Uint32(b) sendWire(t, peer, power, 0, words(1)) } } caps := capability(t, out) if len(notifications) != 2 || wake == 0 || power == 0 || len(caps.Outputs) != 1 || caps.Outputs[0].Name != "HEADLESS-1" || !caps.Outputs[0].PowerSupported { t.Fatalf("initial: notifications=%v wake=%d power=%d caps=%+v", notifications, wake, power, caps) } if !slices.Contains(caps.Missing, "zwp_linux_dmabuf_v1 >= 4") || slices.Contains(caps.Missing, "ext_session_lock_manager_v1 >= 1") { t.Fatal(caps.Missing) } lifetime := caps.Outputs[0].Lifetime sendWire(t, peer, notifications[1000], 0, nil) idle, ok := nextEvent(t, out).(ports.WaylandIdle) if !ok || idle.Generation != 1 || idle.After != time.Second || idle.Cycle != 1 { t.Fatal(idle, ok) } // Reload retains the one-second proxy and removes only the two-second one. commands <- ports.SetDeadlines{Generation: 2, Deadlines: []time.Duration{time.Second, 3 * time.Second}} var added uint32 for i := 0; i < 2; i++ { id, op, b := readWire(t, peer) if id == notifications[2000] && op == 0 { continue } if id == ids["ext_idle_notifier_v1"] && op == 1 && binary.NativeEndian.Uint32(b[4:]) == 3000 { added = binary.NativeEndian.Uint32(b) continue } t.Fatalf("reload request %d/%d %x", id, op, b) } if added == 0 { t.Fatal("new deadline missing") } capability(t, out) // An in-flight event for the deleted notification must never become policy. sendWire(t, peer, notifications[2000], 0, nil) sendWire(t, peer, notifications[1000], 1, nil) sendWire(t, peer, notifications[1000], 0, nil) idle, ok = nextEvent(t, out).(ports.WaylandIdle) if !ok || idle.Generation != 2 || idle.Proxy == 0 || idle.Cycle != 2 { t.Fatal(idle, ok) } commands <- ports.OutputPower{On: false} id, op, b := readWire(t, peer) if id != power || op != 0 || binary.NativeEndian.Uint32(b) != 0 { t.Fatalf("off %d/%d %x", id, op, b) } sendWire(t, peer, wake, 0, nil) sendWire(t, peer, wake, 1, nil) id, op, b = readWire(t, peer) if id != power || op != 0 || binary.NativeEndian.Uint32(b) != 1 { t.Fatalf("wake %d/%d %x", id, op, b) } if _, ok := nextEvent(t, out).(ports.WaylandActivity); !ok { t.Fatal("missing activity") } // Removing/re-adding the same global number creates a new output lifetime. sendWire(t, peer, registry, 1, words(11)) for i := 0; i < 2; i++ { readWire(t, peer) } caps = capability(t, out) if len(caps.Outputs) != 0 { t.Fatal(caps) } global(t, peer, registry, 11, "wl_output", 4) var newPower uint32 for i := 0; i < 2; i++ { id, _, b = readWire(t, peer) if id == registry { ids["new-output"] = binary.NativeEndian.Uint32(b[len(b)-4:]) } else { newPower = binary.NativeEndian.Uint32(b) } } caps = capability(t, out) if len(caps.Outputs) != 1 || caps.Outputs[0].Lifetime == lifetime { t.Fatal(caps) } // A late failed event from the previous output cannot disable its successor. sendWire(t, peer, power, 1, nil) sendWire(t, peer, newPower, 1, nil) id, op, _ = readWire(t, peer) if id != newPower || op != 1 { t.Fatal("failed control not destroyed", id, op) } if failure, ok := nextEvent(t, out).(ports.WaylandPowerFailed); !ok || failure.Output != caps.Outputs[0].Lifetime { t.Fatal(failure, ok) } caps = capability(t, out) if caps.Outputs[0].PowerSupported || len(caps.Missing) != 4 { t.Fatal(caps) } cancel() select { case err := <-done: if err != nil { t.Fatal(err) } case <-time.After(2 * time.Second): t.Fatal("Run failed to join reader") } } func TestMissingAndDisabledCapabilities(t *testing.T) { conn, peer := sockets(t) c, err := New(conn, Options{Requirements: ports.WaylandRequirements{Lock: true, Visual: true}}, zerowrap.New(zerowrap.Config{Output: io.Discard})) if err != nil { t.Fatal(err) } _, _, b := readWire(t, peer) registry := binary.NativeEndian.Uint32(b) ctx, cancel := context.WithCancel(context.Background()) defer cancel() out := make(chan ports.WaylandEvent, 8) done := make(chan error, 1) go func() { done <- c.Run(ctx, nil, out) }() _, _, b = readWire(t, peer) global(t, peer, registry, 10, "ext_idle_notifier_v1", 1) global(t, peer, registry, 11, "zwlr_output_power_manager_v1", 1) synchronize(t, peer, b) id, _, b := readWire(t, peer) if id != 1 { t.Fatalf("disabled optional global bound: %d", id) } synchronize(t, peer, b) caps := capability(t, out) if len(caps.Missing) != 5 || slices.Contains(caps.Missing, "ext_idle_notifier_v1 >= 2") { t.Fatal(caps) } cancel() if err := <-done; err != nil { t.Fatal(err) } } func TestDeadlineValidation(t *testing.T) { for _, d := range []time.Duration{-time.Second, time.Nanosecond, time.Duration(1<<32) * time.Millisecond} { if _, err := normalize([]time.Duration{d}); err == nil { t.Fatal(d) } } ds, err := normalize([]time.Duration{0, time.Second, time.Second}) if err != nil || len(ds) != 1 { t.Fatal(ds, err) } } func TestCancelBlockedStartup(t *testing.T) { conn, peer := sockets(t) c, err := New(conn, Options{}, zerowrap.New(zerowrap.Config{Output: io.Discard})) if err != nil { t.Fatal(err) } readWire(t, peer) ctx, cancel := context.WithCancel(context.Background()) done := make(chan error, 1) go func() { done <- c.Run(ctx, nil, nil) }() readWire(t, peer) // sync request deliberately unanswered cancel() select { case err := <-done: if err != nil { t.Fatal(err) } case <-time.After(time.Second): t.Fatal("cancel did not close socket") } } func TestBoundedCallbackQueueFailsClosed(t *testing.T) { conn, peer := sockets(t) c, err := New(conn, Options{}, zerowrap.New(zerowrap.Config{Output: io.Discard})) if err != nil { t.Fatal(err) } readWire(t, peer) for i := 0; i <= queueSize; i++ { c.enqueue(notice{kind: 1}) } select { case <-c.overflow: default: t.Fatal("overflow not reported") } }