//go:build linux
package wayland
import (
"encoding/binary"
"fmt"
"image"
"image/color"
"image/draw"
"io"
"net"
"os"
"path/filepath"
"sync"
"testing"
"time"
"chirami/internal/platform"
"golang.org/x/sys/unix"
)
func TestBackendContract(t *testing.T) {
var _ platform.Backend = (*backend)(nil)
}
func TestOutputGeometryAndScale(t *testing.T) {
out := &outputInfo{
physW: 3840,
physH: 2160,
outputScale: 2,
}
// 1. Without xdg_output, integer scale 2
if s := out.Scale(); s != 2.0 {
t.Errorf("Scale() = %f, want 2.0", s)
}
if w := out.LogicalWidth(); w != 1920.0 {
t.Errorf("LogicalWidth() = %f, want 1920.0", w)
}
if h := out.LogicalHeight(); h != 1080.0 {
t.Errorf("LogicalHeight() = %f, want 1080.0", h)
}
// 2. With fractional scale (e.g. 1.5)
out.preferredScale = 1.5
if s := out.Scale(); s != 1.5 {
t.Errorf("Scale() with preferred = %f, want 1.5", s)
}
// 3. With explicit xdg_output logical dimensions
out.hasLogical = true
out.logicalW = 2560
out.logicalH = 1440
if w := out.LogicalWidth(); w != 2560.0 {
t.Errorf("LogicalWidth() with logical = %f, want 2560.0", w)
}
if h := out.LogicalHeight(); h != 1440.0 {
t.Errorf("LogicalHeight() with logical = %f, want 1440.0", h)
}
}
func TestWireFramingSplitHeader(t *testing.T) {
fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM, 0)
if err != nil {
t.Fatalf("socketpair: %v", err)
}
serverFile := os.NewFile(uintptr(fds[0]), "server")
clientFile := os.NewFile(uintptr(fds[1]), "client")
defer serverFile.Close()
defer clientFile.Close()
clientConn, err := net.FileConn(clientFile)
if err != nil {
t.Fatalf("FileConn: %v", err)
}
defer clientConn.Close()
unixClient, ok := clientConn.(*net.UnixConn)
if !ok {
t.Fatalf("expected *net.UnixConn")
}
msgCh := make(chan waylandMsg, 16)
go func() {
buf := make([]byte, 0, 16384)
tmp := make([]byte, 8192)
oob := make([]byte, unix.CmsgSpace(4)*8)
var pendingFds []int
for {
for len(buf) >= 8 {
objID := binary.LittleEndian.Uint32(buf[0:4])
opAndSize := binary.LittleEndian.Uint32(buf[4:8])
opcode := opAndSize & 0xffff
sz := int(opAndSize >> 16)
if sz < 8 {
msgCh <- waylandMsg{err: fmt.Errorf("invalid message size %d", sz)}
return
}
if len(buf) < sz {
break
}
recFd := -1
if len(pendingFds) > 0 {
recFd = pendingFds[0]
pendingFds = pendingFds[1:]
}
msgData := make([]byte, sz-8)
copy(msgData, buf[8:sz])
buf = buf[sz:]
msgCh <- waylandMsg{objID: objID, opcode: opcode, fd: recFd, data: msgData}
}
n, oobn, _, _, err := unixClient.ReadMsgUnix(tmp, oob)
if err != nil {
msgCh <- waylandMsg{err: err}
return
}
if n == 0 && oobn == 0 {
msgCh <- waylandMsg{err: io.EOF}
return
}
if oobn > 0 {
scms, err := unix.ParseSocketControlMessage(oob[:oobn])
if err == nil {
for _, scm := range scms {
fds, err := unix.ParseUnixRights(&scm)
if err == nil {
pendingFds = append(pendingFds, fds...)
}
}
}
}
if n > 0 {
buf = append(buf, tmp[:n]...)
}
}
}()
// Send message with split header write: 4 bytes then 4 bytes + 4 bytes payload
var fullMsg [12]byte
binary.LittleEndian.PutUint32(fullMsg[0:4], 42)
binary.LittleEndian.PutUint32(fullMsg[4:8], (12<<16)|1)
binary.LittleEndian.PutUint32(fullMsg[8:12], 100)
// Write first 4 bytes
if _, err := serverFile.Write(fullMsg[:4]); err != nil {
t.Fatalf("write split 1: %v", err)
}
time.Sleep(10 * time.Millisecond)
// Write remaining 8 bytes
if _, err := serverFile.Write(fullMsg[4:]); err != nil {
t.Fatalf("write split 2: %v", err)
}
select {
case m := <-msgCh:
if m.err != nil {
t.Fatalf("unexpected reader error: %v", m.err)
}
if m.objID != 42 || m.opcode != 1 {
t.Errorf("got obj %d op %d, want 42 1", m.objID, m.opcode)
}
if len(m.data) != 4 || binary.LittleEndian.Uint32(m.data) != 100 {
t.Errorf("unexpected payload: %v", m.data)
}
case <-time.After(500 * time.Millisecond):
t.Fatal("timed out waiting for message after split header")
}
}
func TestMockWaylandLifecycle(t *testing.T) {
tmpDir, err := os.MkdirTemp("/tmp", "wltest-*")
if err != nil {
t.Fatalf("MkdirTemp: %v", err)
}
defer os.RemoveAll(tmpDir)
disp := "wayland-0"
sockPath := filepath.Join(tmpDir, disp)
listener, err := net.ListenUnix("unix", &net.UnixAddr{Name: sockPath, Net: "unix"})
if err != nil {
t.Fatalf("listen: %v", err)
}
defer listener.Close()
t.Setenv("XDG_RUNTIME_DIR", tmpDir)
t.Setenv("WAYLAND_DISPLAY", disp)
var wg sync.WaitGroup
wg.Add(1)
// Mock Wayland Compositor Server
go func() {
defer wg.Done()
conn, err := listener.AcceptUnix()
if err != nil {
return
}
defer conn.Close()
sendMsg := func(objID, opcode uint32, args ...any) {
var payload []byte
for _, arg := range args {
switch v := arg.(type) {
case uint32:
var b [4]byte
binary.LittleEndian.PutUint32(b[:], v)
payload = append(payload, b[:]...)
case int32:
var b [4]byte
binary.LittleEndian.PutUint32(b[:], uint32(v))
payload = append(payload, b[:]...)
case string:
payload = append(payload, encodeString(v)...)
}
}
totalLen := 8 + len(payload)
hdr := make([]byte, 8)
binary.LittleEndian.PutUint32(hdr[0:4], objID)
binary.LittleEndian.PutUint32(hdr[4:8], uint32((totalLen<<16)|int(opcode)))
_, _ = conn.Write(append(hdr, payload...))
}
readClientMsg := func() (uint32, uint32, []byte) {
var hdr [8]byte
_, err := io.ReadFull(conn, hdr[:])
if err != nil {
return 0, 0, nil
}
objID := binary.LittleEndian.Uint32(hdr[0:4])
opAndSize := binary.LittleEndian.Uint32(hdr[4:8])
opcode := opAndSize & 0xffff
sz := int(opAndSize >> 16)
var data []byte
if sz > 8 {
data = make([]byte, sz-8)
_, _ = io.ReadFull(conn, data)
}
return objID, opcode, data
}
var registryID uint32
var syncID uint32
var pointerID uint32
var locateSurfID uint32
var locateLayerSurfID uint32
var seatBindID uint32
// 1. Initial registry & sync
for {
obj, op, data := readClientMsg()
if obj == 0 {
return
}
if obj == 1 && op == 1 { // get_registry
registryID = binary.LittleEndian.Uint32(data[0:4])
}
if obj == 1 && op == 0 { // sync
syncID = binary.LittleEndian.Uint32(data[0:4])
break
}
}
// Advertise globals: wl_compositor (1), wl_shm (2), wl_seat (3), zwlr_layer_shell_v1 (4), wp_viewporter (5), wl_output (6)
sendMsg(registryID, 0, uint32(1), "wl_compositor", uint32(4))
sendMsg(registryID, 0, uint32(2), "wl_shm", uint32(1))
sendMsg(registryID, 0, uint32(3), "wl_seat", uint32(2))
sendMsg(registryID, 0, uint32(4), "zwlr_layer_shell_v1", uint32(1))
sendMsg(registryID, 0, uint32(5), "wp_viewporter", uint32(1))
sendMsg(registryID, 0, uint32(6), "wl_output", uint32(2))
// Initial sync done
sendMsg(syncID, 0, uint32(0))
sendMsg(1, 1, syncID) // delete_id
// 2. Consume binds and output geometry
for {
obj, op, data := readClientMsg()
if obj == 0 {
return
}
if obj == 1 && op == 0 { // second sync
syncID = binary.LittleEndian.Uint32(data[0:4])
break
}
if obj == registryID && op == 0 {
global := binary.LittleEndian.Uint32(data[0:4])
bindID := binary.LittleEndian.Uint32(data[len(data)-4:])
if global == 3 {
seatBindID = bindID
sendMsg(bindID, 0, uint32(1)) // capabilities: pointer
}
if global == 6 { // output
sendMsg(bindID, 1, uint32(1), int32(1920), int32(1080), int32(60000))
sendMsg(bindID, 3, int32(1))
}
}
if seatBindID != 0 && obj == seatBindID && op == 0 && len(data) == 4 {
pointerID = binary.LittleEndian.Uint32(data[0:4])
}
}
// Second sync done
sendMsg(syncID, 0, uint32(0))
sendMsg(1, 1, syncID) // delete_id
// Main loop for server
locateArmed := false
for {
obj, op, data := readClientMsg()
if obj == 0 {
return
}
// the client asks for the pointer once it has seen the seat's
// capabilities, which can be after the second sync
if obj == seatBindID && op == 0 && len(data) == 4 {
pointerID = binary.LittleEndian.Uint32(data[0:4])
continue
}
// layer_shell.get_layer_surface (opcode 0 on layerShellID)
if op == 0 && len(data) >= 20 {
createdID := binary.LittleEndian.Uint32(data[0:4])
targetSurf := binary.LittleEndian.Uint32(data[4:8])
name, _, _ := decodeString(data, 16)
if name == "chirami-locate" {
locateLayerSurfID = createdID
locateSurfID = targetSurf
// Send configure on locate layer surface
sendMsg(locateLayerSurfID, 0, uint32(1), uint32(1920), uint32(1080))
} else if name == "chirami-popup" {
sendMsg(createdID, 0, uint32(2), uint32(280), uint32(120))
}
}
// wl_region.add(0, 0, w, h)
if op == 1 && len(data) >= 16 {
locateArmed = true
}
// wl_surface.commit
if obj == locateSurfID && op == 6 && locateArmed {
locateArmed = false
// Send pointer.enter
sendMsg(pointerID, 0, uint32(10), locateSurfID, int32(100*256), int32(200*256))
}
}
}()
runErr := Run(func(b platform.Backend) error {
// Contract checks
x, y, ok := b.Pointer()
if ok || x != 0 || y != 0 {
t.Errorf("Pointer() = (%f, %f, %v), want (0, 0, false)", x, y, ok)
}
// Locate
anc, err := b.Locate()
if err != nil {
t.Fatalf("Locate error: %v", err)
}
if anc.X != 100 || anc.Y != 200 {
t.Errorf("Anchor = (%f, %f), want (100, 200)", anc.X, anc.Y)
}
if anc.Screen.W != 1920 || anc.Screen.H != 1080 {
t.Errorf("Screen = %v, want 1920x1080", anc.Screen)
}
// Show popup
img := image.NewRGBA(image.Rect(0, 0, 280, 120))
draw.Draw(img, img.Bounds(), &image.Uniform{color.RGBA{255, 0, 0, 255}}, image.Point{}, draw.Src)
err = b.Show(platform.Popup{
Image: img,
X: anc.X,
Y: anc.Y,
Hit: platform.Rect{X: 0, Y: 0, W: 280, H: 120},
})
if err != nil {
t.Fatalf("Show error: %v", err)
}
// Hide
b.Hide()
return nil
})
if runErr != nil {
t.Fatalf("Run returned error: %v", runErr)
}
listener.Close()
wg.Wait()
}
// Growing the pool for a bigger card must resize it (opcode 2), not destroy
// it (opcode 1): the next create_buffer on a destroyed pool is a protocol
// error that kills the connection.
func TestShmPoolGrowResizes(t *testing.T) {
fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM, 0)
if err != nil {
t.Fatal(err)
}
conn := func(fd int) *net.UnixConn {
c, err := net.FileConn(os.NewFile(uintptr(fd), "sock"))
if err != nil {
t.Fatal(err)
}
return c.(*net.UnixConn)
}
client, server := conn(fds[0]), conn(fds[1])
defer client.Close()
defer server.Close()
next := uint32(10)
alloc := func() uint32 { next++; return next }
p := newShmPool(3)
defer p.destroy(client)
if _, _, _, err := p.getBuffer(client, 100, 100, alloc, nil); err != nil {
t.Fatal(err)
}
p.handleRelease(p.slots[0].bufID)
if _, _, _, err := p.getBuffer(client, 400, 400, alloc, nil); err != nil {
t.Fatal(err)
}
client.SetReadDeadline(time.Now().Add(time.Second))
server.SetReadDeadline(time.Now().Add(time.Second))
buf := make([]byte, 4096)
oob := make([]byte, 64)
var msgs []byte
for len(msgs) < 16+32+12+8+32 { // create_pool, create_buffer, resize, destroy buffer, create_buffer
n, _, _, _, err := server.ReadMsgUnix(buf, oob)
if err != nil {
t.Fatal(err)
}
msgs = append(msgs, buf[:n]...)
}
for off := 0; off+8 <= len(msgs); {
obj := binary.LittleEndian.Uint32(msgs[off:])
hdr := binary.LittleEndian.Uint32(msgs[off+4:])
size, op := int(hdr>>16), hdr&0xffff
if obj == p.poolID && op == 1 {
t.Fatal("the pool was destroyed while growing it")
}
off += size
}
}