detached 6e7bbcd

test(protocol): cover negotiated binds and headless feedback lifecycle

commit
6e7bbcda70e41f1efe25e719ab4f332645addbbd
author
brice · · 2026-09-28 07:58 UTC
parent
5856658
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · 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+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 8

↑↓ moveenter openesc close