test(protocol): cover negotiated binds and headless feedback lifecycle
- commit
- 6e7bbcda70e41f1efe25e719ab4f332645addbbd
- author
- brice · · 2026-09-28 07:58 UTC
- parent
- 5856658
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
3 files · +317 -0
A protocol/format_table_test.go +71 -0
| @@ -0,0 +1,71 @@ | |||
| 1 | + | //go:build linux | |
| 2 | + | ||
| 3 | + | package protocol_test | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "errors" | |
| 7 | + | "github.com/bnema/wlturbo" | |
| 8 | + | "github.com/bnema/wlturbo/protocol/linuxdmabuf" | |
| 9 | + | "golang.org/x/sys/unix" | |
| 10 | + | "io" | |
| 11 | + | "os" | |
| 12 | + | "testing" | |
| 13 | + | ) | |
| 14 | + | ||
| 15 | + | // Exercise format_table through generated event dispatch so the received FD | |
| 16 | + | // has real transport ownership; verify closing by inspecting the taken FD. | |
| 17 | + | func TestMalformedFormatTables(t *testing.T) { | |
| 18 | + | for _, tc := range []struct { | |
| 19 | + | name string | |
| 20 | + | contents []byte | |
| 21 | + | size uint32 | |
| 22 | + | wantErr bool | |
| 23 | + | }{ | |
| 24 | + | {"unaligned", make([]byte, 16), 17, true}, | |
| 25 | + | {"truncated", make([]byte, 16), 32, true}, | |
| 26 | + | {"zero", nil, 0, false}, | |
| 27 | + | {"oversize", nil, (16 << 20) + 16, true}, | |
| 28 | + | } { | |
| 29 | + | t.Run(tc.name, func(t *testing.T) { | |
| 30 | + | c, p := pair(t) | |
| 31 | + | d, e := wlturbo.ConnectFromConn(c) | |
| 32 | + | noerr(t, e) | |
| 33 | + | defer d.Close() | |
| 34 | + | request(t, p) | |
| 35 | + | fb := linuxdmabuf.NewLinuxDmabufFeedback(d.Context()) | |
| 36 | + | fb.SetID(d.AllocateID()) | |
| 37 | + | d.Context().Register(fb) | |
| 38 | + | file, e := os.CreateTemp(t.TempDir(), "table") | |
| 39 | + | noerr(t, e) | |
| 40 | + | defer file.Close() | |
| 41 | + | _, e = file.Write(tc.contents) | |
| 42 | + | noerr(t, e) | |
| 43 | + | called := false | |
| 44 | + | fb.OnFormatTable(func(fd *wlturbo.OwnedFD, size uint32) { | |
| 45 | + | called = true | |
| 46 | + | entries, err := linuxdmabuf.ReadFormatTable(fd, size) | |
| 47 | + | if (err != nil) != tc.wantErr { | |
| 48 | + | t.Fatalf("size=%d entries=%v err=%v", size, entries, err) | |
| 49 | + | } | |
| 50 | + | if tc.name == "truncated" && !errors.Is(err, io.ErrUnexpectedEOF) { | |
| 51 | + | t.Fatalf("short read: %v", err) | |
| 52 | + | } | |
| 53 | + | // Take/Close must be idempotent, including after a failed size check. | |
| 54 | + | if _, err = fd.Take(); err == nil { | |
| 55 | + | t.Fatal("already consumed fd still takeable") | |
| 56 | + | } | |
| 57 | + | noerr(t, fd.Close()) | |
| 58 | + | noerr(t, fd.Close()) | |
| 59 | + | }) | |
| 60 | + | send(t, p, msg(fb.ID(), 1, tc.size), int(file.Fd())) | |
| 61 | + | noerr(t, d.Dispatch()) | |
| 62 | + | if !called { | |
| 63 | + | t.Fatal("no callback") | |
| 64 | + | } | |
| 65 | + | // The transport FD was a duplicate: caller's descriptor is still open. | |
| 66 | + | if _, e = unix.FcntlInt(file.Fd(), unix.F_GETFD, 0); e != nil { | |
| 67 | + | t.Fatalf("sender fd closed: %v", e) | |
| 68 | + | } | |
| 69 | + | }) | |
| 70 | + | } | |
| 71 | + | } | |
| @@ -0,0 +1,71 @@ | |||||
| 1 | + | //go:build linux | |||
| 2 | + | ||||
| 3 | + | package protocol_test | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "errors" | |||
| 7 | + | "github.com/bnema/wlturbo" | |||
| 8 | + | "github.com/bnema/wlturbo/protocol/linuxdmabuf" | |||
| 9 | + | "golang.org/x/sys/unix" | |||
| 10 | + | "io" | |||
| 11 | + | "os" | |||
| 12 | + | "testing" | |||
| 13 | + | ) | |||
| 14 | + | ||||
| 15 | + | // Exercise format_table through generated event dispatch so the received FD | |||
| 16 | + | // has real transport ownership; verify closing by inspecting the taken FD. | |||
| 17 | + | func TestMalformedFormatTables(t *testing.T) { | |||
| 18 | + | for _, tc := range []struct { | |||
| 19 | + | name string | |||
| 20 | + | contents []byte | |||
| 21 | + | size uint32 | |||
| 22 | + | wantErr bool | |||
| 23 | + | }{ | |||
| 24 | + | {"unaligned", make([]byte, 16), 17, true}, | |||
| 25 | + | {"truncated", make([]byte, 16), 32, true}, | |||
| 26 | + | {"zero", nil, 0, false}, | |||
| 27 | + | {"oversize", nil, (16 << 20) + 16, true}, | |||
| 28 | + | } { | |||
| 29 | + | t.Run(tc.name, func(t *testing.T) { | |||
| 30 | + | c, p := pair(t) | |||
| 31 | + | d, e := wlturbo.ConnectFromConn(c) | |||
| 32 | + | noerr(t, e) | |||
| 33 | + | defer d.Close() | |||
| 34 | + | request(t, p) | |||
| 35 | + | fb := linuxdmabuf.NewLinuxDmabufFeedback(d.Context()) | |||
| 36 | + | fb.SetID(d.AllocateID()) | |||
| 37 | + | d.Context().Register(fb) | |||
| 38 | + | file, e := os.CreateTemp(t.TempDir(), "table") | |||
| 39 | + | noerr(t, e) | |||
| 40 | + | defer file.Close() | |||
| 41 | + | _, e = file.Write(tc.contents) | |||
| 42 | + | noerr(t, e) | |||
| 43 | + | called := false | |||
| 44 | + | fb.OnFormatTable(func(fd *wlturbo.OwnedFD, size uint32) { | |||
| 45 | + | called = true | |||
| 46 | + | entries, err := linuxdmabuf.ReadFormatTable(fd, size) | |||
| 47 | + | if (err != nil) != tc.wantErr { | |||
| 48 | + | t.Fatalf("size=%d entries=%v err=%v", size, entries, err) | |||
| 49 | + | } | |||
| 50 | + | if tc.name == "truncated" && !errors.Is(err, io.ErrUnexpectedEOF) { | |||
| 51 | + | t.Fatalf("short read: %v", err) | |||
| 52 | + | } | |||
| 53 | + | // Take/Close must be idempotent, including after a failed size check. | |||
| 54 | + | if _, err = fd.Take(); err == nil { | |||
| 55 | + | t.Fatal("already consumed fd still takeable") | |||
| 56 | + | } | |||
| 57 | + | noerr(t, fd.Close()) | |||
| 58 | + | noerr(t, fd.Close()) | |||
| 59 | + | }) | |||
| 60 | + | send(t, p, msg(fb.ID(), 1, tc.size), int(file.Fd())) | |||
| 61 | + | noerr(t, d.Dispatch()) | |||
| 62 | + | if !called { | |||
| 63 | + | t.Fatal("no callback") | |||
| 64 | + | } | |||
| 65 | + | // The transport FD was a duplicate: caller's descriptor is still open. | |||
| 66 | + | if _, e = unix.FcntlInt(file.Fd(), unix.F_GETFD, 0); e != nil { | |||
| 67 | + | t.Fatalf("sender fd closed: %v", e) | |||
| 68 | + | } | |||
| 69 | + | }) | |||
| 70 | + | } | |||
| 71 | + | } | |||
A protocol/headless_test.go +189 -0
| @@ -0,0 +1,189 @@ | |||
| 1 | + | //go:build linux | |
| 2 | + | ||
| 3 | + | package protocol_test | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "context" | |
| 7 | + | "fmt" | |
| 8 | + | "net" | |
| 9 | + | "os" | |
| 10 | + | "os/exec" | |
| 11 | + | "path/filepath" | |
| 12 | + | "strings" | |
| 13 | + | "syscall" | |
| 14 | + | "testing" | |
| 15 | + | "time" | |
| 16 | + | ||
| 17 | + | "github.com/bnema/wlturbo" | |
| 18 | + | "github.com/bnema/wlturbo/protocol/core" | |
| 19 | + | "github.com/bnema/wlturbo/protocol/drmsyncobj" | |
| 20 | + | "github.com/bnema/wlturbo/protocol/fractionalscale" | |
| 21 | + | "github.com/bnema/wlturbo/protocol/linuxdmabuf" | |
| 22 | + | "github.com/bnema/wlturbo/protocol/viewporter" | |
| 23 | + | "github.com/bnema/wlturbo/protocol/xdgshell" | |
| 24 | + | ) | |
| 25 | + | ||
| 26 | + | func TestHeadlessNeferWL(t *testing.T) { | |
| 27 | + | binary := os.Getenv("WLTURBO_HEADLESS") | |
| 28 | + | if binary == "" { | |
| 29 | + | t.Skip("set WLTURBO_HEADLESS to a NeferWL binary") | |
| 30 | + | } | |
| 31 | + | binary, e := filepath.Abs(binary) | |
| 32 | + | noerr(t, e) | |
| 33 | + | runtimeDir := filepath.Join(t.TempDir(), "run") | |
| 34 | + | noerr(t, os.Mkdir(runtimeDir, 0700)) | |
| 35 | + | configDir := filepath.Join(t.TempDir(), "config") | |
| 36 | + | noerr(t, os.Mkdir(configDir, 0700)) | |
| 37 | + | ctx, cancel := context.WithTimeout(context.Background(), 18*time.Second) | |
| 38 | + | defer cancel() | |
| 39 | + | cmd := exec.Command(binary, "--backend=headless", "--no-terminal", "--no-xwayland", "--size", "640x480", "--timeout", "20s") | |
| 40 | + | cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} | |
| 41 | + | cmd.Env = append(cleanDisplayEnv(os.Environ()), "XDG_RUNTIME_DIR="+runtimeDir, "XDG_CONFIG_HOME="+configDir) | |
| 42 | + | log, e := os.Create(filepath.Join(t.TempDir(), "neferwl.log")) | |
| 43 | + | noerr(t, e) | |
| 44 | + | defer log.Close() | |
| 45 | + | cmd.Stdout = log | |
| 46 | + | cmd.Stderr = log | |
| 47 | + | noerr(t, cmd.Start()) | |
| 48 | + | t.Cleanup(func() { _ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL); _ = cmd.Wait() }) | |
| 49 | + | // A failed startup must still terminate its process group and reap it. | |
| 50 | + | socket := "" | |
| 51 | + | for socket == "" { | |
| 52 | + | if e := ctx.Err(); e != nil { | |
| 53 | + | t.Fatalf("headless socket timeout: %v\n%s", e, headlessLog(log)) | |
| 54 | + | } | |
| 55 | + | paths, _ := filepath.Glob(filepath.Join(runtimeDir, "wayland-*")) | |
| 56 | + | for _, path := range paths { | |
| 57 | + | info, e := os.Stat(path) | |
| 58 | + | if e == nil && info.Mode()&os.ModeSocket != 0 { | |
| 59 | + | socket = path | |
| 60 | + | break | |
| 61 | + | } | |
| 62 | + | } | |
| 63 | + | if socket == "" { | |
| 64 | + | time.Sleep(20 * time.Millisecond) | |
| 65 | + | } | |
| 66 | + | } | |
| 67 | + | conn, e := net.DialTimeout("unix", socket, time.Second) | |
| 68 | + | noerr(t, e) | |
| 69 | + | d, e := wlturbo.ConnectFromConn(conn) | |
| 70 | + | noerr(t, e) | |
| 71 | + | defer d.Close() | |
| 72 | + | // Close the socket when the test deadline expires so blocking Dispatch exits. | |
| 73 | + | stop := context.AfterFunc(ctx, func() { _ = d.Close() }) | |
| 74 | + | defer stop() | |
| 75 | + | noerr(t, d.Roundtrip()) | |
| 76 | + | require := func(iface string) wlturbo.Global { | |
| 77 | + | t.Helper() | |
| 78 | + | g, ok := d.Registry().FindGlobal(iface) | |
| 79 | + | if !ok { | |
| 80 | + | t.Fatalf("required global %s absent\n%s", iface, headlessLog(log)) | |
| 81 | + | } | |
| 82 | + | t.Logf("%s server=%d", iface, g.Version) | |
| 83 | + | return g | |
| 84 | + | } | |
| 85 | + | bind := func(iface string, supported uint32, proxy wlturbo.Proxy) uint32 { | |
| 86 | + | t.Helper() | |
| 87 | + | g := require(iface) | |
| 88 | + | v := g.Version | |
| 89 | + | if v > supported { | |
| 90 | + | v = supported | |
| 91 | + | } | |
| 92 | + | noerr(t, d.Registry().Bind(g.Name, iface, v, proxy)) | |
| 93 | + | t.Logf("%s negotiated=%d", iface, v) | |
| 94 | + | return v | |
| 95 | + | } | |
| 96 | + | compositor := core.NewCompositor(d.Context()) | |
| 97 | + | bind(core.CompositorInterface, 6, compositor) | |
| 98 | + | wm := xdgshell.NewXdgWmBase(d.Context()) | |
| 99 | + | bind(xdgshell.XdgWmBaseInterface, 7, wm) | |
| 100 | + | dm := linuxdmabuf.NewLinuxDmabuf(d.Context()) | |
| 101 | + | if bind(linuxdmabuf.LinuxDmabufInterface, 4, dm) < 4 { | |
| 102 | + | t.Fatal("dmabuf feedback requires v4") | |
| 103 | + | } | |
| 104 | + | syncManager := drmsyncobj.NewWpLinuxDrmSyncobjManager(d.Context()) | |
| 105 | + | bind(drmsyncobj.WpLinuxDrmSyncobjManagerInterface, 1, syncManager) | |
| 106 | + | vp := viewporter.NewWpViewporter(d.Context()) | |
| 107 | + | bind(viewporter.WpViewporterInterface, 1, vp) | |
| 108 | + | if g, ok := d.Registry().FindGlobal(fractionalscale.WpFractionalScaleManagerInterface); ok { | |
| 109 | + | fs := fractionalscale.NewWpFractionalScaleManager(d.Context()) | |
| 110 | + | v := g.Version | |
| 111 | + | if v > 1 { | |
| 112 | + | v = 1 | |
| 113 | + | } | |
| 114 | + | noerr(t, d.Registry().Bind(g.Name, g.Interface, v, fs)) | |
| 115 | + | t.Logf("%s negotiated=%d", g.Interface, v) | |
| 116 | + | defer func() { noerr(t, fs.Destroy()) }() | |
| 117 | + | } else { | |
| 118 | + | t.Log("fractional scale absent") | |
| 119 | + | } | |
| 120 | + | surface, e := compositor.CreateSurface() | |
| 121 | + | noerr(t, e) | |
| 122 | + | xs, e := wm.GetXdgSurface(surface) | |
| 123 | + | noerr(t, e) | |
| 124 | + | top, e := xs.GetToplevel() | |
| 125 | + | noerr(t, e) | |
| 126 | + | feedback, e := dm.GetSurfaceFeedback(surface) | |
| 127 | + | noerr(t, e) | |
| 128 | + | ss, e := syncManager.GetSurface(surface) | |
| 129 | + | noerr(t, e) | |
| 130 | + | configured, done := false, false | |
| 131 | + | top.OnConfigure(func(w, h int32, states []byte) { t.Logf("xdg_toplevel.configure %dx%d", w, h) }) | |
| 132 | + | xs.OnConfigure(func(serial uint32) { | |
| 133 | + | noerr(t, xs.AckConfigure(serial)) | |
| 134 | + | configured = true | |
| 135 | + | t.Logf("xdg_surface.configure ack=%d", serial) | |
| 136 | + | }) | |
| 137 | + | feedback.OnMainDevice(func(b []byte) { t.Logf("main_device=%x", b) }) | |
| 138 | + | feedback.OnFormatTable(func(fd *wlturbo.OwnedFD, size uint32) { | |
| 139 | + | entries, e := linuxdmabuf.ReadFormatTable(fd, size) | |
| 140 | + | if e != nil { | |
| 141 | + | t.Errorf("format table: %v", e) | |
| 142 | + | } else { | |
| 143 | + | t.Logf("format-table entries=%d", len(entries)) | |
| 144 | + | } | |
| 145 | + | }) | |
| 146 | + | feedback.OnDone(func() { done = true }) | |
| 147 | + | // Initial empty commit requests xdg configure; no buffer or window is shown. | |
| 148 | + | noerr(t, surface.Commit()) | |
| 149 | + | for (!configured || !done) && ctx.Err() == nil { | |
| 150 | + | if e := d.Dispatch(); e != nil { | |
| 151 | + | t.Fatalf("dispatch: %v\n%s", e, headlessLog(log)) | |
| 152 | + | } | |
| 153 | + | } | |
| 154 | + | if !configured || !done { | |
| 155 | + | t.Fatalf("configure=%t feedback=%t: %v\n%s", configured, done, ctx.Err(), headlessLog(log)) | |
| 156 | + | } | |
| 157 | + | noerr(t, surface.Commit()) | |
| 158 | + | // Drop all child objects while their parents are still alive. | |
| 159 | + | noerr(t, ss.Destroy()) | |
| 160 | + | noerr(t, feedback.Destroy()) | |
| 161 | + | noerr(t, top.Destroy()) | |
| 162 | + | noerr(t, xs.Destroy()) | |
| 163 | + | noerr(t, surface.Destroy()) | |
| 164 | + | noerr(t, vp.Destroy()) | |
| 165 | + | noerr(t, syncManager.Destroy()) | |
| 166 | + | noerr(t, dm.Destroy()) | |
| 167 | + | noerr(t, wm.Destroy()) | |
| 168 | + | noerr(t, d.Close()) | |
| 169 | + | } | |
| 170 | + | func cleanDisplayEnv(in []string) []string { | |
| 171 | + | out := make([]string, 0, len(in)) | |
| 172 | + | for _, e := range in { | |
| 173 | + | if strings.HasPrefix(e, "DISPLAY=") || strings.HasPrefix(e, "WAYLAND_DISPLAY=") || strings.HasPrefix(e, "XDG_RUNTIME_DIR=") || strings.HasPrefix(e, "XDG_CONFIG_HOME=") { | |
| 174 | + | continue | |
| 175 | + | } | |
| 176 | + | out = append(out, e) | |
| 177 | + | } | |
| 178 | + | return out | |
| 179 | + | } | |
| 180 | + | func headlessLog(f *os.File) string { | |
| 181 | + | b, e := os.ReadFile(f.Name()) | |
| 182 | + | if e != nil { | |
| 183 | + | return fmt.Sprint(e) | |
| 184 | + | } | |
| 185 | + | if len(b) > 8192 { | |
| 186 | + | b = b[len(b)-8192:] | |
| 187 | + | } | |
| 188 | + | return string(b) | |
| 189 | + | } | |
| @@ -0,0 +1,189 @@ | |||||
| 1 | + | //go:build linux | |||
| 2 | + | ||||
| 3 | + | package protocol_test | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "context" | |||
| 7 | + | "fmt" | |||
| 8 | + | "net" | |||
| 9 | + | "os" | |||
| 10 | + | "os/exec" | |||
| 11 | + | "path/filepath" | |||
| 12 | + | "strings" | |||
| 13 | + | "syscall" | |||
| 14 | + | "testing" | |||
| 15 | + | "time" | |||
| 16 | + | ||||
| 17 | + | "github.com/bnema/wlturbo" | |||
| 18 | + | "github.com/bnema/wlturbo/protocol/core" | |||
| 19 | + | "github.com/bnema/wlturbo/protocol/drmsyncobj" | |||
| 20 | + | "github.com/bnema/wlturbo/protocol/fractionalscale" | |||
| 21 | + | "github.com/bnema/wlturbo/protocol/linuxdmabuf" | |||
| 22 | + | "github.com/bnema/wlturbo/protocol/viewporter" | |||
| 23 | + | "github.com/bnema/wlturbo/protocol/xdgshell" | |||
| 24 | + | ) | |||
| 25 | + | ||||
| 26 | + | func TestHeadlessNeferWL(t *testing.T) { | |||
| 27 | + | binary := os.Getenv("WLTURBO_HEADLESS") | |||
| 28 | + | if binary == "" { | |||
| 29 | + | t.Skip("set WLTURBO_HEADLESS to a NeferWL binary") | |||
| 30 | + | } | |||
| 31 | + | binary, e := filepath.Abs(binary) | |||
| 32 | + | noerr(t, e) | |||
| 33 | + | runtimeDir := filepath.Join(t.TempDir(), "run") | |||
| 34 | + | noerr(t, os.Mkdir(runtimeDir, 0700)) | |||
| 35 | + | configDir := filepath.Join(t.TempDir(), "config") | |||
| 36 | + | noerr(t, os.Mkdir(configDir, 0700)) | |||
| 37 | + | ctx, cancel := context.WithTimeout(context.Background(), 18*time.Second) | |||
| 38 | + | defer cancel() | |||
| 39 | + | cmd := exec.Command(binary, "--backend=headless", "--no-terminal", "--no-xwayland", "--size", "640x480", "--timeout", "20s") | |||
| 40 | + | cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} | |||
| 41 | + | cmd.Env = append(cleanDisplayEnv(os.Environ()), "XDG_RUNTIME_DIR="+runtimeDir, "XDG_CONFIG_HOME="+configDir) | |||
| 42 | + | log, e := os.Create(filepath.Join(t.TempDir(), "neferwl.log")) | |||
| 43 | + | noerr(t, e) | |||
| 44 | + | defer log.Close() | |||
| 45 | + | cmd.Stdout = log | |||
| 46 | + | cmd.Stderr = log | |||
| 47 | + | noerr(t, cmd.Start()) | |||
| 48 | + | t.Cleanup(func() { _ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL); _ = cmd.Wait() }) | |||
| 49 | + | // A failed startup must still terminate its process group and reap it. | |||
| 50 | + | socket := "" | |||
| 51 | + | for socket == "" { | |||
| 52 | + | if e := ctx.Err(); e != nil { | |||
| 53 | + | t.Fatalf("headless socket timeout: %v\n%s", e, headlessLog(log)) | |||
| 54 | + | } | |||
| 55 | + | paths, _ := filepath.Glob(filepath.Join(runtimeDir, "wayland-*")) | |||
| 56 | + | for _, path := range paths { | |||
| 57 | + | info, e := os.Stat(path) | |||
| 58 | + | if e == nil && info.Mode()&os.ModeSocket != 0 { | |||
| 59 | + | socket = path | |||
| 60 | + | break | |||
| 61 | + | } | |||
| 62 | + | } | |||
| 63 | + | if socket == "" { | |||
| 64 | + | time.Sleep(20 * time.Millisecond) | |||
| 65 | + | } | |||
| 66 | + | } | |||
| 67 | + | conn, e := net.DialTimeout("unix", socket, time.Second) | |||
| 68 | + | noerr(t, e) | |||
| 69 | + | d, e := wlturbo.ConnectFromConn(conn) | |||
| 70 | + | noerr(t, e) | |||
| 71 | + | defer d.Close() | |||
| 72 | + | // Close the socket when the test deadline expires so blocking Dispatch exits. | |||
| 73 | + | stop := context.AfterFunc(ctx, func() { _ = d.Close() }) | |||
| 74 | + | defer stop() | |||
| 75 | + | noerr(t, d.Roundtrip()) | |||
| 76 | + | require := func(iface string) wlturbo.Global { | |||
| 77 | + | t.Helper() | |||
| 78 | + | g, ok := d.Registry().FindGlobal(iface) | |||
| 79 | + | if !ok { | |||
| 80 | + | t.Fatalf("required global %s absent\n%s", iface, headlessLog(log)) | |||
| 81 | + | } | |||
| 82 | + | t.Logf("%s server=%d", iface, g.Version) | |||
| 83 | + | return g | |||
| 84 | + | } | |||
| 85 | + | bind := func(iface string, supported uint32, proxy wlturbo.Proxy) uint32 { | |||
| 86 | + | t.Helper() | |||
| 87 | + | g := require(iface) | |||
| 88 | + | v := g.Version | |||
| 89 | + | if v > supported { | |||
| 90 | + | v = supported | |||
| 91 | + | } | |||
| 92 | + | noerr(t, d.Registry().Bind(g.Name, iface, v, proxy)) | |||
| 93 | + | t.Logf("%s negotiated=%d", iface, v) | |||
| 94 | + | return v | |||
| 95 | + | } | |||
| 96 | + | compositor := core.NewCompositor(d.Context()) | |||
| 97 | + | bind(core.CompositorInterface, 6, compositor) | |||
| 98 | + | wm := xdgshell.NewXdgWmBase(d.Context()) | |||
| 99 | + | bind(xdgshell.XdgWmBaseInterface, 7, wm) | |||
| 100 | + | dm := linuxdmabuf.NewLinuxDmabuf(d.Context()) | |||
| 101 | + | if bind(linuxdmabuf.LinuxDmabufInterface, 4, dm) < 4 { | |||
| 102 | + | t.Fatal("dmabuf feedback requires v4") | |||
| 103 | + | } | |||
| 104 | + | syncManager := drmsyncobj.NewWpLinuxDrmSyncobjManager(d.Context()) | |||
| 105 | + | bind(drmsyncobj.WpLinuxDrmSyncobjManagerInterface, 1, syncManager) | |||
| 106 | + | vp := viewporter.NewWpViewporter(d.Context()) | |||
| 107 | + | bind(viewporter.WpViewporterInterface, 1, vp) | |||
| 108 | + | if g, ok := d.Registry().FindGlobal(fractionalscale.WpFractionalScaleManagerInterface); ok { | |||
| 109 | + | fs := fractionalscale.NewWpFractionalScaleManager(d.Context()) | |||
| 110 | + | v := g.Version | |||
| 111 | + | if v > 1 { | |||
| 112 | + | v = 1 | |||
| 113 | + | } | |||
| 114 | + | noerr(t, d.Registry().Bind(g.Name, g.Interface, v, fs)) | |||
| 115 | + | t.Logf("%s negotiated=%d", g.Interface, v) | |||
| 116 | + | defer func() { noerr(t, fs.Destroy()) }() | |||
| 117 | + | } else { | |||
| 118 | + | t.Log("fractional scale absent") | |||
| 119 | + | } | |||
| 120 | + | surface, e := compositor.CreateSurface() | |||
| 121 | + | noerr(t, e) | |||
| 122 | + | xs, e := wm.GetXdgSurface(surface) | |||
| 123 | + | noerr(t, e) | |||
| 124 | + | top, e := xs.GetToplevel() | |||
| 125 | + | noerr(t, e) | |||
| 126 | + | feedback, e := dm.GetSurfaceFeedback(surface) | |||
| 127 | + | noerr(t, e) | |||
| 128 | + | ss, e := syncManager.GetSurface(surface) | |||
| 129 | + | noerr(t, e) | |||
| 130 | + | configured, done := false, false | |||
| 131 | + | top.OnConfigure(func(w, h int32, states []byte) { t.Logf("xdg_toplevel.configure %dx%d", w, h) }) | |||
| 132 | + | xs.OnConfigure(func(serial uint32) { | |||
| 133 | + | noerr(t, xs.AckConfigure(serial)) | |||
| 134 | + | configured = true | |||
| 135 | + | t.Logf("xdg_surface.configure ack=%d", serial) | |||
| 136 | + | }) | |||
| 137 | + | feedback.OnMainDevice(func(b []byte) { t.Logf("main_device=%x", b) }) | |||
| 138 | + | feedback.OnFormatTable(func(fd *wlturbo.OwnedFD, size uint32) { | |||
| 139 | + | entries, e := linuxdmabuf.ReadFormatTable(fd, size) | |||
| 140 | + | if e != nil { | |||
| 141 | + | t.Errorf("format table: %v", e) | |||
| 142 | + | } else { | |||
| 143 | + | t.Logf("format-table entries=%d", len(entries)) | |||
| 144 | + | } | |||
| 145 | + | }) | |||
| 146 | + | feedback.OnDone(func() { done = true }) | |||
| 147 | + | // Initial empty commit requests xdg configure; no buffer or window is shown. | |||
| 148 | + | noerr(t, surface.Commit()) | |||
| 149 | + | for (!configured || !done) && ctx.Err() == nil { | |||
| 150 | + | if e := d.Dispatch(); e != nil { | |||
| 151 | + | t.Fatalf("dispatch: %v\n%s", e, headlessLog(log)) | |||
| 152 | + | } | |||
| 153 | + | } | |||
| 154 | + | if !configured || !done { | |||
| 155 | + | t.Fatalf("configure=%t feedback=%t: %v\n%s", configured, done, ctx.Err(), headlessLog(log)) | |||
| 156 | + | } | |||
| 157 | + | noerr(t, surface.Commit()) | |||
| 158 | + | // Drop all child objects while their parents are still alive. | |||
| 159 | + | noerr(t, ss.Destroy()) | |||
| 160 | + | noerr(t, feedback.Destroy()) | |||
| 161 | + | noerr(t, top.Destroy()) | |||
| 162 | + | noerr(t, xs.Destroy()) | |||
| 163 | + | noerr(t, surface.Destroy()) | |||
| 164 | + | noerr(t, vp.Destroy()) | |||
| 165 | + | noerr(t, syncManager.Destroy()) | |||
| 166 | + | noerr(t, dm.Destroy()) | |||
| 167 | + | noerr(t, wm.Destroy()) | |||
| 168 | + | noerr(t, d.Close()) | |||
| 169 | + | } | |||
| 170 | + | func cleanDisplayEnv(in []string) []string { | |||
| 171 | + | out := make([]string, 0, len(in)) | |||
| 172 | + | for _, e := range in { | |||
| 173 | + | if strings.HasPrefix(e, "DISPLAY=") || strings.HasPrefix(e, "WAYLAND_DISPLAY=") || strings.HasPrefix(e, "XDG_RUNTIME_DIR=") || strings.HasPrefix(e, "XDG_CONFIG_HOME=") { | |||
| 174 | + | continue | |||
| 175 | + | } | |||
| 176 | + | out = append(out, e) | |||
| 177 | + | } | |||
| 178 | + | return out | |||
| 179 | + | } | |||
| 180 | + | func headlessLog(f *os.File) string { | |||
| 181 | + | b, e := os.ReadFile(f.Name()) | |||
| 182 | + | if e != nil { | |||
| 183 | + | return fmt.Sprint(e) | |||
| 184 | + | } | |||
| 185 | + | if len(b) > 8192 { | |||
| 186 | + | b = b[len(b)-8192:] | |||
| 187 | + | } | |||
| 188 | + | return string(b) | |||
| 189 | + | } | |||
A protocol/negotiation_test.go +57 -0
| @@ -0,0 +1,57 @@ | |||
| 1 | + | //go:build linux | |
| 2 | + | ||
| 3 | + | package protocol_test | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "encoding/binary" | |
| 7 | + | "github.com/bnema/wlturbo" | |
| 8 | + | "github.com/bnema/wlturbo/protocol/xdgshell" | |
| 9 | + | "testing" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | // bindAnnounced selects a supported version using the registry's announcement. | |
| 13 | + | func bindAnnounced(d *wlturbo.Display, iface string, supported uint32, proxy wlturbo.Proxy) (uint32, error) { | |
| 14 | + | g, ok := d.Registry().FindGlobal(iface) | |
| 15 | + | if !ok { | |
| 16 | + | return 0, nil | |
| 17 | + | } | |
| 18 | + | version := g.Version | |
| 19 | + | if version > supported { | |
| 20 | + | version = supported | |
| 21 | + | } | |
| 22 | + | if version == 0 { | |
| 23 | + | return 0, nil | |
| 24 | + | } | |
| 25 | + | return version, d.Registry().Bind(g.Name, iface, version, proxy) | |
| 26 | + | } | |
| 27 | + | func TestAnnouncedVersionNegotiation(t *testing.T) { | |
| 28 | + | c, p := pair(t) | |
| 29 | + | d, e := wlturbo.ConnectFromConn(c) | |
| 30 | + | noerr(t, e) | |
| 31 | + | defer d.Close() | |
| 32 | + | request(t, p) | |
| 33 | + | iface := xdgshell.XdgWmBaseInterface | |
| 34 | + | if v, e := bindAnnounced(d, iface, 6, xdgshell.NewXdgWmBase(d.Context())); e != nil || v != 0 { | |
| 35 | + | t.Fatalf("missing: %d %v", v, e) | |
| 36 | + | } | |
| 37 | + | for i, version := range []uint32{2, 9} { | |
| 38 | + | name := uint32(30 + i) | |
| 39 | + | announcement := append(msg(0, 0, name)[8:], str(iface)...) | |
| 40 | + | announcement = append(announcement, msg(0, 0, version)[8:]...) | |
| 41 | + | send(t, p, payload(d.Registry().ID(), 0, announcement...)) | |
| 42 | + | noerr(t, d.Dispatch()) | |
| 43 | + | proxy := xdgshell.NewXdgWmBase(d.Context()) | |
| 44 | + | negotiated, e := bindAnnounced(d, iface, 6, proxy) | |
| 45 | + | noerr(t, e) | |
| 46 | + | want := version | |
| 47 | + | if want > 6 { | |
| 48 | + | want = 6 | |
| 49 | + | } | |
| 50 | + | _, op, body := request(t, p) | |
| 51 | + | if op != 0 || negotiated != want || binary.NativeEndian.Uint32(body) != name || binary.NativeEndian.Uint32(body[len(body)-8:]) != want || binary.NativeEndian.Uint32(body[len(body)-4:]) != proxy.ID() { | |
| 52 | + | t.Fatalf("server=%d negotiated=%d wire=%x", version, negotiated, body) | |
| 53 | + | } | |
| 54 | + | send(t, p, msg(d.Registry().ID(), 1, name)) | |
| 55 | + | noerr(t, d.Dispatch()) | |
| 56 | + | } | |
| 57 | + | } | |
| @@ -0,0 +1,57 @@ | |||||
| 1 | + | //go:build linux | |||
| 2 | + | ||||
| 3 | + | package protocol_test | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "encoding/binary" | |||
| 7 | + | "github.com/bnema/wlturbo" | |||
| 8 | + | "github.com/bnema/wlturbo/protocol/xdgshell" | |||
| 9 | + | "testing" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | // bindAnnounced selects a supported version using the registry's announcement. | |||
| 13 | + | func bindAnnounced(d *wlturbo.Display, iface string, supported uint32, proxy wlturbo.Proxy) (uint32, error) { | |||
| 14 | + | g, ok := d.Registry().FindGlobal(iface) | |||
| 15 | + | if !ok { | |||
| 16 | + | return 0, nil | |||
| 17 | + | } | |||
| 18 | + | version := g.Version | |||
| 19 | + | if version > supported { | |||
| 20 | + | version = supported | |||
| 21 | + | } | |||
| 22 | + | if version == 0 { | |||
| 23 | + | return 0, nil | |||
| 24 | + | } | |||
| 25 | + | return version, d.Registry().Bind(g.Name, iface, version, proxy) | |||
| 26 | + | } | |||
| 27 | + | func TestAnnouncedVersionNegotiation(t *testing.T) { | |||
| 28 | + | c, p := pair(t) | |||
| 29 | + | d, e := wlturbo.ConnectFromConn(c) | |||
| 30 | + | noerr(t, e) | |||
| 31 | + | defer d.Close() | |||
| 32 | + | request(t, p) | |||
| 33 | + | iface := xdgshell.XdgWmBaseInterface | |||
| 34 | + | if v, e := bindAnnounced(d, iface, 6, xdgshell.NewXdgWmBase(d.Context())); e != nil || v != 0 { | |||
| 35 | + | t.Fatalf("missing: %d %v", v, e) | |||
| 36 | + | } | |||
| 37 | + | for i, version := range []uint32{2, 9} { | |||
| 38 | + | name := uint32(30 + i) | |||
| 39 | + | announcement := append(msg(0, 0, name)[8:], str(iface)...) | |||
| 40 | + | announcement = append(announcement, msg(0, 0, version)[8:]...) | |||
| 41 | + | send(t, p, payload(d.Registry().ID(), 0, announcement...)) | |||
| 42 | + | noerr(t, d.Dispatch()) | |||
| 43 | + | proxy := xdgshell.NewXdgWmBase(d.Context()) | |||
| 44 | + | negotiated, e := bindAnnounced(d, iface, 6, proxy) | |||
| 45 | + | noerr(t, e) | |||
| 46 | + | want := version | |||
| 47 | + | if want > 6 { | |||
| 48 | + | want = 6 | |||
| 49 | + | } | |||
| 50 | + | _, op, body := request(t, p) | |||
| 51 | + | if op != 0 || negotiated != want || binary.NativeEndian.Uint32(body) != name || binary.NativeEndian.Uint32(body[len(body)-8:]) != want || binary.NativeEndian.Uint32(body[len(body)-4:]) != proxy.ID() { | |||
| 52 | + | t.Fatalf("server=%d negotiated=%d wire=%x", version, negotiated, body) | |||
| 53 | + | } | |||
| 54 | + | send(t, p, msg(d.Registry().ID(), 1, name)) | |||
| 55 | + | noerr(t, d.Dispatch()) | |||
| 56 | + | } | |||
| 57 | + | } | |||