test(protocol): exercise extension lifecycle and feedback over socketpair
- commit
- 5856658fbad5922b599855ddeb9c4937d166dc72
- author
- brice · · 2026-09-28 07:55 UTC
- parent
- 975d460
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +576 -1
M consumer_test.go +13 -1
| @@ -35,7 +35,19 @@ func TestExternalConsumer(t *testing.T) { | |||
| 35 | 35 | if err := os.WriteFile(filepath.Join(temp, "consumer_test.go"), source, 0600); err != nil { | |
| 36 | 36 | t.Fatal(err) | |
| 37 | 37 | } | |
| 38 | - | cmd := exec.Command(goBin, "test", "-run", "TestGeneratedCoreOverSocketpair|TestBufferReleaseAndOfferChild", "./...") | |
| 38 | + | // Keep the extension fake server in a separate package: both fixtures | |
| 39 | + | // independently own their socketpair helpers. | |
| 40 | + | extension, err := os.ReadFile(filepath.Join(root, "protocol/extensions_test.go")) | |
| 41 | + | if err != nil { | |
| 42 | + | t.Fatal(err) | |
| 43 | + | } | |
| 44 | + | if err := os.Mkdir(filepath.Join(temp, "extensions"), 0700); err != nil { | |
| 45 | + | t.Fatal(err) | |
| 46 | + | } | |
| 47 | + | if err := os.WriteFile(filepath.Join(temp, "extensions", "extensions_test.go"), extension, 0600); err != nil { | |
| 48 | + | t.Fatal(err) | |
| 49 | + | } | |
| 50 | + | cmd := exec.Command(goBin, "test", "-run", "TestGeneratedCoreOverSocketpair|TestBufferReleaseAndOfferChild|TestExtensionsOverSocketpair", "./...") | |
| 39 | 51 | cmd.Dir = temp | |
| 40 | 52 | cmd.Env = append(os.Environ(), "GOWORK=off", "GOFLAGS=-mod=mod") | |
| 41 | 53 | if output, err := cmd.CombinedOutput(); err != nil { | |
| @@ -35,7 +35,19 @@ func TestExternalConsumer(t *testing.T) { | |||||
| 35 | if err := os.WriteFile(filepath.Join(temp, "consumer_test.go"), source, 0600); err != nil { | 35 | if err := os.WriteFile(filepath.Join(temp, "consumer_test.go"), source, 0600); err != nil { | ||
| 36 | t.Fatal(err) | 36 | t.Fatal(err) | ||
| 37 | } | 37 | } | ||
| 38 | - | cmd := exec.Command(goBin, "test", "-run", "TestGeneratedCoreOverSocketpair|TestBufferReleaseAndOfferChild", "./...") | 38 | + | // Keep the extension fake server in a separate package: both fixtures |
| 39 | + | // independently own their socketpair helpers. | |||
| 40 | + | extension, err := os.ReadFile(filepath.Join(root, "protocol/extensions_test.go")) | |||
| 41 | + | if err != nil { | |||
| 42 | + | t.Fatal(err) | |||
| 43 | + | } | |||
| 44 | + | if err := os.Mkdir(filepath.Join(temp, "extensions"), 0700); err != nil { | |||
| 45 | + | t.Fatal(err) | |||
| 46 | + | } | |||
| 47 | + | if err := os.WriteFile(filepath.Join(temp, "extensions", "extensions_test.go"), extension, 0600); err != nil { | |||
| 48 | + | t.Fatal(err) | |||
| 49 | + | } | |||
| 50 | + | cmd := exec.Command(goBin, "test", "-run", "TestGeneratedCoreOverSocketpair|TestBufferReleaseAndOfferChild|TestExtensionsOverSocketpair", "./...") | |||
| 39 | cmd.Dir = temp | 51 | cmd.Dir = temp | ||
| 40 | cmd.Env = append(os.Environ(), "GOWORK=off", "GOFLAGS=-mod=mod") | 52 | cmd.Env = append(os.Environ(), "GOWORK=off", "GOFLAGS=-mod=mod") | ||
| 41 | if output, err := cmd.CombinedOutput(); err != nil { | 53 | if output, err := cmd.CombinedOutput(); err != nil { | ||
A protocol/extensions_test.go +467 -0
| @@ -0,0 +1,467 @@ | |||
| 1 | + | //go:build linux | |
| 2 | + | ||
| 3 | + | package protocol_test | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "encoding/binary" | |
| 7 | + | "net" | |
| 8 | + | "os" | |
| 9 | + | "strings" | |
| 10 | + | "testing" | |
| 11 | + | "time" | |
| 12 | + | ||
| 13 | + | "github.com/bnema/wlturbo" | |
| 14 | + | "github.com/bnema/wlturbo/protocol/core" | |
| 15 | + | "github.com/bnema/wlturbo/protocol/drmsyncobj" | |
| 16 | + | "github.com/bnema/wlturbo/protocol/fractionalscale" | |
| 17 | + | "github.com/bnema/wlturbo/protocol/linuxdmabuf" | |
| 18 | + | "github.com/bnema/wlturbo/protocol/textinput" | |
| 19 | + | "github.com/bnema/wlturbo/protocol/viewporter" | |
| 20 | + | "github.com/bnema/wlturbo/protocol/xdgshell" | |
| 21 | + | "golang.org/x/sys/unix" | |
| 22 | + | ) | |
| 23 | + | ||
| 24 | + | func pair(t *testing.T) (*net.UnixConn, *net.UnixConn) { | |
| 25 | + | t.Helper() | |
| 26 | + | fds, e := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | |
| 27 | + | if e != nil { | |
| 28 | + | t.Fatal(e) | |
| 29 | + | } | |
| 30 | + | a := os.NewFile(uintptr(fds[0]), "a") | |
| 31 | + | b := os.NewFile(uintptr(fds[1]), "b") | |
| 32 | + | ca, e := net.FileConn(a) | |
| 33 | + | a.Close() | |
| 34 | + | if e != nil { | |
| 35 | + | t.Fatal(e) | |
| 36 | + | } | |
| 37 | + | cb, e := net.FileConn(b) | |
| 38 | + | b.Close() | |
| 39 | + | if e != nil { | |
| 40 | + | t.Fatal(e) | |
| 41 | + | } | |
| 42 | + | t.Cleanup(func() { ca.Close(); cb.Close() }) | |
| 43 | + | return ca.(*net.UnixConn), cb.(*net.UnixConn) | |
| 44 | + | } | |
| 45 | + | func msg(id uint32, op uint16, body ...uint32) []byte { | |
| 46 | + | b := make([]byte, 8+4*len(body)) | |
| 47 | + | binary.NativeEndian.PutUint32(b, id) | |
| 48 | + | binary.NativeEndian.PutUint32(b[4:], uint32(len(b))<<16|uint32(op)) | |
| 49 | + | for i, v := range body { | |
| 50 | + | binary.NativeEndian.PutUint32(b[8+i*4:], v) | |
| 51 | + | } | |
| 52 | + | return b | |
| 53 | + | } | |
| 54 | + | func payload(id uint32, op uint16, b ...byte) []byte { | |
| 55 | + | p := make([]byte, 8+len(b)) | |
| 56 | + | copy(p, msg(id, op)) | |
| 57 | + | copy(p[8:], b) | |
| 58 | + | binary.NativeEndian.PutUint32(p[4:], uint32(len(p))<<16|uint32(op)) | |
| 59 | + | return p | |
| 60 | + | } | |
| 61 | + | func arr(b []byte) []byte { | |
| 62 | + | out := make([]byte, 4+(len(b)+3)&^3) | |
| 63 | + | binary.NativeEndian.PutUint32(out, uint32(len(b))) | |
| 64 | + | copy(out[4:], b) | |
| 65 | + | return out | |
| 66 | + | } | |
| 67 | + | func str(s string) []byte { return arr(append([]byte(s), 0)) } | |
| 68 | + | func send(t *testing.T, p *net.UnixConn, b []byte, fd ...int) { | |
| 69 | + | t.Helper() | |
| 70 | + | var rights []byte | |
| 71 | + | if len(fd) > 0 { | |
| 72 | + | rights = unix.UnixRights(fd...) | |
| 73 | + | } | |
| 74 | + | if _, _, e := p.WriteMsgUnix(b, rights, nil); e != nil { | |
| 75 | + | t.Fatal(e) | |
| 76 | + | } | |
| 77 | + | } | |
| 78 | + | func request(t *testing.T, p *net.UnixConn) (uint32, uint16, []byte) { | |
| 79 | + | t.Helper() | |
| 80 | + | p.SetReadDeadline(time.Now().Add(2 * time.Second)) | |
| 81 | + | b := make([]byte, 4096) | |
| 82 | + | n, e := p.Read(b) | |
| 83 | + | if e != nil { | |
| 84 | + | t.Fatal(e) | |
| 85 | + | } | |
| 86 | + | if n < 8 { | |
| 87 | + | t.Fatal("short request") | |
| 88 | + | } | |
| 89 | + | return binary.NativeEndian.Uint32(b), uint16(binary.NativeEndian.Uint32(b[4:])), b[8:n] | |
| 90 | + | } | |
| 91 | + | func noerr(t *testing.T, e error) { | |
| 92 | + | t.Helper() | |
| 93 | + | if e != nil { | |
| 94 | + | t.Fatal(e) | |
| 95 | + | } | |
| 96 | + | } | |
| 97 | + | func bind(t *testing.T, d *wlturbo.Display, p *net.UnixConn, name uint32, iface string, supported uint32, proxy wlturbo.Proxy) { | |
| 98 | + | t.Helper() | |
| 99 | + | v := supported | |
| 100 | + | if v > 4 { | |
| 101 | + | v = 4 | |
| 102 | + | } | |
| 103 | + | noerr(t, d.Registry().Bind(name, iface, v, proxy)) | |
| 104 | + | _, op, b := request(t, p) | |
| 105 | + | if op != 0 || binary.NativeEndian.Uint32(b) != name || binary.NativeEndian.Uint32(b[len(b)-8:]) != v || binary.NativeEndian.Uint32(b[len(b)-4:]) != proxy.ID() { | |
| 106 | + | t.Fatalf("bad bind %s version=%d %x", iface, v, b) | |
| 107 | + | } | |
| 108 | + | } | |
| 109 | + | func TestExtensionsOverSocketpair(t *testing.T) { | |
| 110 | + | c, p := pair(t) | |
| 111 | + | d, e := wlturbo.ConnectFromConn(c) | |
| 112 | + | noerr(t, e) | |
| 113 | + | defer d.Close() | |
| 114 | + | request(t, p) | |
| 115 | + | // A registry announcement is the only input to optional-global discovery. | |
| 116 | + | send(t, p, payload(d.Registry().ID(), 0, append(msg(0, 0, 11)[8:12], append(str(fractionalscale.WpFractionalScaleManagerInterface), msg(0, 0, 1)[8:12]...)...)...)) | |
| 117 | + | noerr(t, d.Dispatch()) | |
| 118 | + | if g, ok := d.Registry().FindGlobal(fractionalscale.WpFractionalScaleManagerInterface); !ok || g.Version != 1 { | |
| 119 | + | t.Fatal("announced fractional scale missing", g, ok) | |
| 120 | + | } | |
| 121 | + | if _, ok := d.Registry().FindGlobal(textinput.TextInputManagerV3Interface); ok { | |
| 122 | + | t.Fatal("absent text input") | |
| 123 | + | } | |
| 124 | + | surface := core.NewSurface(d.Context()) | |
| 125 | + | surface.SetID(d.AllocateID()) | |
| 126 | + | d.Context().Register(surface) | |
| 127 | + | seat := core.NewSeat(d.Context()) | |
| 128 | + | seat.SetID(d.AllocateID()) | |
| 129 | + | d.Context().Register(seat) | |
| 130 | + | wm := xdgshell.NewXdgWmBase(d.Context()) | |
| 131 | + | bind(t, d, p, 1, xdgshell.XdgWmBaseInterface, 7, wm) | |
| 132 | + | xs, e := wm.GetXdgSurface(surface) | |
| 133 | + | noerr(t, e) | |
| 134 | + | request(t, p) | |
| 135 | + | top, e := xs.GetToplevel() | |
| 136 | + | noerr(t, e) | |
| 137 | + | request(t, p) | |
| 138 | + | var sequence []string | |
| 139 | + | top.OnConfigure(func(w, h int32, states []byte) { | |
| 140 | + | if w != 800 || h != 600 { | |
| 141 | + | t.Fatalf("size %d,%d", w, h) | |
| 142 | + | } | |
| 143 | + | sequence = append(sequence, "top") | |
| 144 | + | }) | |
| 145 | + | xs.OnConfigure(func(serial uint32) { sequence = append(sequence, "surface"); noerr(t, xs.AckConfigure(serial)) }) | |
| 146 | + | send(t, p, payload(top.ID(), 0, append(msg(0, 0, 800, 600)[8:], arr(nil)...)...)) | |
| 147 | + | noerr(t, d.Dispatch()) | |
| 148 | + | send(t, p, msg(xs.ID(), 0, 9)) | |
| 149 | + | noerr(t, d.Dispatch()) | |
| 150 | + | _, op, b := request(t, p) | |
| 151 | + | if op != 4 || binary.NativeEndian.Uint32(b) != 9 || strings.Join(sequence, ",") != "top,surface" { | |
| 152 | + | t.Fatalf("xdg %d %x %v", op, b, sequence) | |
| 153 | + | } | |
| 154 | + | noerr(t, top.Destroy()) | |
| 155 | + | request(t, p) | |
| 156 | + | noerr(t, xs.Destroy()) | |
| 157 | + | request(t, p) | |
| 158 | + | noerr(t, wm.Destroy()) | |
| 159 | + | request(t, p) | |
| 160 | + | dm := linuxdmabuf.NewLinuxDmabuf(d.Context()) | |
| 161 | + | bind(t, d, p, 2, linuxdmabuf.LinuxDmabufInterface, 6, dm) | |
| 162 | + | fb, e := dm.GetDefaultFeedback() | |
| 163 | + | noerr(t, e) | |
| 164 | + | request(t, p) | |
| 165 | + | table, e := os.CreateTemp(t.TempDir(), "table") | |
| 166 | + | noerr(t, e) | |
| 167 | + | var bytes [32]byte | |
| 168 | + | binary.NativeEndian.PutUint32(bytes[:], 0x34325258) | |
| 169 | + | binary.NativeEndian.PutUint64(bytes[8:], 19) | |
| 170 | + | binary.NativeEndian.PutUint32(bytes[16:], 0x34325241) | |
| 171 | + | binary.NativeEndian.PutUint64(bytes[24:], 42) | |
| 172 | + | _, e = table.Write(bytes[:]) | |
| 173 | + | noerr(t, e) | |
| 174 | + | defer table.Close() | |
| 175 | + | var entries []linuxdmabuf.FormatEntry | |
| 176 | + | var indices []uint16 | |
| 177 | + | var feedback []string | |
| 178 | + | fb.OnMainDevice(func(dev []byte) { | |
| 179 | + | if len(dev) != 8 { | |
| 180 | + | t.Fatalf("device %x", dev) | |
| 181 | + | } | |
| 182 | + | feedback = append(feedback, "main") | |
| 183 | + | }) | |
| 184 | + | fb.OnFormatTable(func(fd *wlturbo.OwnedFD, size uint32) { | |
| 185 | + | entries, e = linuxdmabuf.ReadFormatTable(fd, size) | |
| 186 | + | noerr(t, e) | |
| 187 | + | feedback = append(feedback, "table") | |
| 188 | + | }) | |
| 189 | + | fb.OnTrancheTargetDevice(func(dev []byte) { feedback = append(feedback, "target") }) | |
| 190 | + | fb.OnTrancheFormats(func(data []byte) { | |
| 191 | + | indices, e = linuxdmabuf.TrancheIndices(data, len(entries)) | |
| 192 | + | noerr(t, e) | |
| 193 | + | feedback = append(feedback, "formats") | |
| 194 | + | }) | |
| 195 | + | fb.OnTrancheFlags(func(flags uint32) { | |
| 196 | + | if flags != 1 { | |
| 197 | + | t.Fatal(flags) | |
| 198 | + | } | |
| 199 | + | feedback = append(feedback, "flags") | |
| 200 | + | }) | |
| 201 | + | fb.OnTrancheDone(func() { feedback = append(feedback, "tranche") }) | |
| 202 | + | fb.OnDone(func() { feedback = append(feedback, "done") }) | |
| 203 | + | send(t, p, payload(fb.ID(), 2, arr(make([]byte, 8))...)) | |
| 204 | + | noerr(t, d.Dispatch()) | |
| 205 | + | send(t, p, msg(fb.ID(), 1, 32), int(table.Fd())) | |
| 206 | + | noerr(t, d.Dispatch()) | |
| 207 | + | send(t, p, payload(fb.ID(), 4, arr(make([]byte, 8))...)) | |
| 208 | + | noerr(t, d.Dispatch()) | |
| 209 | + | send(t, p, payload(fb.ID(), 5, arr([]byte{0, 0, 1, 0})...)) | |
| 210 | + | noerr(t, d.Dispatch()) | |
| 211 | + | send(t, p, msg(fb.ID(), 6, 1)) | |
| 212 | + | noerr(t, d.Dispatch()) | |
| 213 | + | send(t, p, msg(fb.ID(), 3)) | |
| 214 | + | noerr(t, d.Dispatch()) | |
| 215 | + | send(t, p, msg(fb.ID(), 0)) | |
| 216 | + | noerr(t, d.Dispatch()) | |
| 217 | + | if len(entries) != 2 || entries[1].Modifier != 42 || len(indices) != 2 || indices[1] != 1 || strings.Join(feedback, ",") != "main,table,target,formats,flags,tranche,done" { | |
| 218 | + | t.Fatalf("feedback %v %v %v", entries, indices, feedback) | |
| 219 | + | } | |
| 220 | + | noerr(t, fb.Destroy()) | |
| 221 | + | request(t, p) | |
| 222 | + | params, e := dm.CreateParams() | |
| 223 | + | noerr(t, e) | |
| 224 | + | request(t, p) | |
| 225 | + | f, e := os.Open("/dev/null") | |
| 226 | + | noerr(t, e) | |
| 227 | + | noerr(t, params.Add(int(f.Fd()), 0, 0, 4, 0, 0)) | |
| 228 | + | request(t, p) | |
| 229 | + | if _, e := unix.FcntlInt(f.Fd(), unix.F_GETFD, 0); e == nil { | |
| 230 | + | t.Fatal("sent fd not closed") | |
| 231 | + | } | |
| 232 | + | f.Close() | |
| 233 | + | var buffer *core.Buffer | |
| 234 | + | params.OnCreated(func(b *core.Buffer) { buffer = b }) | |
| 235 | + | params.OnFailed(func() { sequence = append(sequence, "failed") }) | |
| 236 | + | noerr(t, params.Create(1, 1, 0x34325258, 0)) | |
| 237 | + | request(t, p) | |
| 238 | + | send(t, p, msg(params.ID(), 0, 999)) | |
| 239 | + | noerr(t, d.Dispatch()) | |
| 240 | + | if buffer == nil || buffer.ID() != 999 { | |
| 241 | + | t.Fatal("typed buffer missing") | |
| 242 | + | } | |
| 243 | + | noerr(t, buffer.Destroy()) | |
| 244 | + | request(t, p) | |
| 245 | + | noerr(t, params.Destroy()) | |
| 246 | + | request(t, p) | |
| 247 | + | failedParams, e := dm.CreateParams() | |
| 248 | + | noerr(t, e) | |
| 249 | + | request(t, p) | |
| 250 | + | failedParams.OnFailed(func() { sequence = append(sequence, "failed") }) | |
| 251 | + | noerr(t, failedParams.Create(1, 1, 0x34325258, 0)) | |
| 252 | + | request(t, p) | |
| 253 | + | send(t, p, msg(failedParams.ID(), 1)) | |
| 254 | + | noerr(t, d.Dispatch()) | |
| 255 | + | if sequence[len(sequence)-1] != "failed" { | |
| 256 | + | t.Fatal(sequence) | |
| 257 | + | } | |
| 258 | + | noerr(t, failedParams.Destroy()) | |
| 259 | + | request(t, p) | |
| 260 | + | immedParams, e := dm.CreateParams() | |
| 261 | + | noerr(t, e) | |
| 262 | + | request(t, p) | |
| 263 | + | immediate, e := immedParams.CreateImmed(1, 1, 0x34325258, 0) | |
| 264 | + | noerr(t, e) | |
| 265 | + | request(t, p) | |
| 266 | + | noerr(t, immediate.Destroy()) | |
| 267 | + | request(t, p) | |
| 268 | + | noerr(t, immedParams.Destroy()) | |
| 269 | + | request(t, p) | |
| 270 | + | noerr(t, dm.Destroy()) | |
| 271 | + | request(t, p) | |
| 272 | + | sync := drmsyncobj.NewWpLinuxDrmSyncobjManager(d.Context()) | |
| 273 | + | bind(t, d, p, 3, drmsyncobj.WpLinuxDrmSyncobjManagerInterface, 1, sync) | |
| 274 | + | ss, e := sync.GetSurface(surface) | |
| 275 | + | noerr(t, e) | |
| 276 | + | request(t, p) | |
| 277 | + | f, e = os.Open("/dev/null") | |
| 278 | + | noerr(t, e) | |
| 279 | + | tl, e := sync.ImportTimeline(int(f.Fd())) | |
| 280 | + | noerr(t, e) | |
| 281 | + | request(t, p) | |
| 282 | + | if _, e := unix.FcntlInt(f.Fd(), unix.F_GETFD, 0); e == nil { | |
| 283 | + | t.Fatal("timeline fd not closed") | |
| 284 | + | } | |
| 285 | + | f.Close() | |
| 286 | + | noerr(t, ss.SetAcquirePoint(tl, 0x123, 0x456)) | |
| 287 | + | _, op, b = request(t, p) | |
| 288 | + | if op != 1 || binary.NativeEndian.Uint32(b[4:]) != 0x123 || binary.NativeEndian.Uint32(b[8:]) != 0x456 { | |
| 289 | + | t.Fatalf("acquire %d %x", op, b) | |
| 290 | + | } | |
| 291 | + | noerr(t, ss.SetReleasePoint(tl, 0x789, 0xabc)) | |
| 292 | + | _, op, b = request(t, p) | |
| 293 | + | if op != 2 || binary.NativeEndian.Uint32(b[4:]) != 0x789 { | |
| 294 | + | t.Fatalf("release %d %x", op, b) | |
| 295 | + | } | |
| 296 | + | noerr(t, ss.Destroy()) | |
| 297 | + | request(t, p) | |
| 298 | + | noerr(t, tl.Destroy()) | |
| 299 | + | request(t, p) | |
| 300 | + | noerr(t, sync.Destroy()) | |
| 301 | + | request(t, p) | |
| 302 | + | vp := viewporter.NewWpViewporter(d.Context()) | |
| 303 | + | bind(t, d, p, 4, viewporter.WpViewporterInterface, 1, vp) | |
| 304 | + | view, e := vp.GetViewport(surface) | |
| 305 | + | noerr(t, e) | |
| 306 | + | request(t, p) | |
| 307 | + | noerr(t, view.Destroy()) | |
| 308 | + | request(t, p) | |
| 309 | + | noerr(t, vp.Destroy()) | |
| 310 | + | request(t, p) | |
| 311 | + | fs := fractionalscale.NewWpFractionalScaleManager(d.Context()) | |
| 312 | + | bind(t, d, p, 5, fractionalscale.WpFractionalScaleManagerInterface, 1, fs) | |
| 313 | + | scale, e := fs.GetFractionalScale(surface) | |
| 314 | + | noerr(t, e) | |
| 315 | + | request(t, p) | |
| 316 | + | var preferred uint32 | |
| 317 | + | scale.OnPreferredScale(func(v uint32) { preferred = v }) | |
| 318 | + | send(t, p, msg(scale.ID(), 0, 150)) | |
| 319 | + | noerr(t, d.Dispatch()) | |
| 320 | + | if preferred != 150 { | |
| 321 | + | t.Fatal(preferred) | |
| 322 | + | } | |
| 323 | + | noerr(t, scale.Destroy()) | |
| 324 | + | request(t, p) | |
| 325 | + | noerr(t, fs.Destroy()) | |
| 326 | + | request(t, p) | |
| 327 | + | ti := textinput.NewTextInputManagerV3(d.Context()) | |
| 328 | + | bind(t, d, p, 6, textinput.TextInputManagerV3Interface, 1, ti) | |
| 329 | + | input, e := ti.GetTextInput(seat) | |
| 330 | + | noerr(t, e) | |
| 331 | + | request(t, p) | |
| 332 | + | noerr(t, input.Enable()) | |
| 333 | + | request(t, p) | |
| 334 | + | noerr(t, input.SetSurroundingText("hello", 5, 5)) | |
| 335 | + | request(t, p) | |
| 336 | + | noerr(t, input.Commit()) | |
| 337 | + | request(t, p) | |
| 338 | + | var events []string | |
| 339 | + | input.OnPreeditString(func(s string, _, _ int32) { events = append(events, "preedit:"+s) }) | |
| 340 | + | input.OnCommitString(func(s string) { events = append(events, "commit:"+s) }) | |
| 341 | + | input.OnDeleteSurroundingText(func(_, _ uint32) { events = append(events, "delete") }) | |
| 342 | + | input.OnDone(func(uint32) { events = append(events, "done") }) | |
| 343 | + | send(t, p, payload(input.ID(), 2, append(str("é"), msg(0, 0, 0, 0)[8:]...)...)) | |
| 344 | + | noerr(t, d.Dispatch()) | |
| 345 | + | send(t, p, payload(input.ID(), 3, str("hi")...)) | |
| 346 | + | noerr(t, d.Dispatch()) | |
| 347 | + | send(t, p, msg(input.ID(), 4, 1, 2)) | |
| 348 | + | noerr(t, d.Dispatch()) | |
| 349 | + | send(t, p, msg(input.ID(), 5, 1)) | |
| 350 | + | noerr(t, d.Dispatch()) | |
| 351 | + | if strings.Join(events, ",") != "preedit:é,commit:hi,delete,done" { | |
| 352 | + | t.Fatal(events) | |
| 353 | + | } | |
| 354 | + | noerr(t, input.Disable()) | |
| 355 | + | request(t, p) | |
| 356 | + | noerr(t, input.Commit()) | |
| 357 | + | request(t, p) | |
| 358 | + | noerr(t, input.Destroy()) | |
| 359 | + | request(t, p) | |
| 360 | + | noerr(t, ti.Destroy()) | |
| 361 | + | request(t, p) | |
| 362 | + | // Core data-device remains in protocol/core; verify its typed child and | |
| 363 | + | // both directions of clipboard descriptor ownership. | |
| 364 | + | mgr := core.NewDataDeviceManager(d.Context()) | |
| 365 | + | bind(t, d, p, 7, core.DataDeviceManagerInterface, 3, mgr) | |
| 366 | + | device, e := mgr.GetDataDevice(seat) | |
| 367 | + | noerr(t, e) | |
| 368 | + | request(t, p) | |
| 369 | + | source, e := mgr.CreateDataSource() | |
| 370 | + | noerr(t, e) | |
| 371 | + | request(t, p) | |
| 372 | + | var offer *core.DataOffer | |
| 373 | + | device.OnDataOffer(func(o *core.DataOffer) { offer = o }) | |
| 374 | + | send(t, p, msg(device.ID(), 0, 998)) | |
| 375 | + | noerr(t, d.Dispatch()) | |
| 376 | + | if offer == nil || offer.ID() != 998 { | |
| 377 | + | t.Fatal("offer not typed") | |
| 378 | + | } | |
| 379 | + | read, write, e := os.Pipe() | |
| 380 | + | noerr(t, e) | |
| 381 | + | defer read.Close() | |
| 382 | + | noerr(t, offer.Receive("text/plain", int(write.Fd()))) | |
| 383 | + | request(t, p) | |
| 384 | + | if _, e := unix.FcntlInt(write.Fd(), unix.F_GETFD, 0); e == nil { | |
| 385 | + | t.Fatal("clipboard sent fd retained") | |
| 386 | + | } | |
| 387 | + | write.Close() | |
| 388 | + | source.OnSend(func(mime string, fd *wlturbo.OwnedFD) { | |
| 389 | + | if mime != "text/plain" { | |
| 390 | + | t.Fatal(mime) | |
| 391 | + | } | |
| 392 | + | n, e := fd.Take() | |
| 393 | + | noerr(t, e) | |
| 394 | + | unix.Close(n) | |
| 395 | + | }) | |
| 396 | + | received, e := os.Open("/dev/null") | |
| 397 | + | noerr(t, e) | |
| 398 | + | send(t, p, payload(source.ID(), 1, str("text/plain")...), int(received.Fd())) | |
| 399 | + | noerr(t, d.Dispatch()) | |
| 400 | + | received.Close() | |
| 401 | + | noerr(t, offer.Destroy()) | |
| 402 | + | request(t, p) | |
| 403 | + | noerr(t, source.Destroy()) | |
| 404 | + | request(t, p) | |
| 405 | + | noerr(t, device.Release()) | |
| 406 | + | request(t, p) | |
| 407 | + | noerr(t, mgr.Release()) | |
| 408 | + | request(t, p) | |
| 409 | + | ||
| 410 | + | } | |
| 411 | + | ||
| 412 | + | func TestLiveFeedback(t *testing.T) { | |
| 413 | + | if os.Getenv("WLTURBO_LIVE") != "1" { | |
| 414 | + | t.Skip("set WLTURBO_LIVE=1") | |
| 415 | + | } | |
| 416 | + | d, e := wlturbo.Connect("") | |
| 417 | + | noerr(t, e) | |
| 418 | + | defer d.Close() | |
| 419 | + | noerr(t, d.Roundtrip()) | |
| 420 | + | for _, iface := range []string{xdgshell.XdgWmBaseInterface, linuxdmabuf.LinuxDmabufInterface, drmsyncobj.WpLinuxDrmSyncobjManagerInterface, viewporter.WpViewporterInterface, fractionalscale.WpFractionalScaleManagerInterface, textinput.TextInputManagerV3Interface} { | |
| 421 | + | g, ok := d.Registry().FindGlobal(iface) | |
| 422 | + | if !ok { | |
| 423 | + | t.Logf("%s absent", iface) | |
| 424 | + | continue | |
| 425 | + | } | |
| 426 | + | v := g.Version | |
| 427 | + | cap := uint32(1) | |
| 428 | + | if iface == xdgshell.XdgWmBaseInterface { | |
| 429 | + | cap = 7 | |
| 430 | + | } | |
| 431 | + | if iface == linuxdmabuf.LinuxDmabufInterface { | |
| 432 | + | cap = 4 | |
| 433 | + | } | |
| 434 | + | if v > cap { | |
| 435 | + | v = cap | |
| 436 | + | } | |
| 437 | + | t.Logf("%s negotiated=%d (server=%d)", iface, v, g.Version) | |
| 438 | + | if iface != linuxdmabuf.LinuxDmabufInterface { | |
| 439 | + | continue | |
| 440 | + | } | |
| 441 | + | if v < 4 { | |
| 442 | + | t.Fatal("dmabuf feedback requires v4") | |
| 443 | + | } | |
| 444 | + | dm := linuxdmabuf.NewLinuxDmabuf(d.Context()) | |
| 445 | + | noerr(t, d.Registry().Bind(g.Name, iface, v, dm)) | |
| 446 | + | fb, e := dm.GetDefaultFeedback() | |
| 447 | + | noerr(t, e) | |
| 448 | + | done := false | |
| 449 | + | fb.OnMainDevice(func(b []byte) { t.Logf("main_device=%x", b) }) | |
| 450 | + | fb.OnFormatTable(func(fd *wlturbo.OwnedFD, size uint32) { | |
| 451 | + | entries, err := linuxdmabuf.ReadFormatTable(fd, size) | |
| 452 | + | if err != nil { | |
| 453 | + | t.Errorf("table: %v", err) | |
| 454 | + | } | |
| 455 | + | t.Logf("format-table entries=%d", len(entries)) | |
| 456 | + | }) | |
| 457 | + | fb.OnDone(func() { done = true }) | |
| 458 | + | for i := 0; i < 100 && !done; i++ { | |
| 459 | + | noerr(t, d.Dispatch()) | |
| 460 | + | } | |
| 461 | + | if !done { | |
| 462 | + | t.Fatal("no feedback done") | |
| 463 | + | } | |
| 464 | + | noerr(t, fb.Destroy()) | |
| 465 | + | noerr(t, dm.Destroy()) | |
| 466 | + | } | |
| 467 | + | } | |
| @@ -0,0 +1,467 @@ | |||||
| 1 | + | //go:build linux | |||
| 2 | + | ||||
| 3 | + | package protocol_test | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "encoding/binary" | |||
| 7 | + | "net" | |||
| 8 | + | "os" | |||
| 9 | + | "strings" | |||
| 10 | + | "testing" | |||
| 11 | + | "time" | |||
| 12 | + | ||||
| 13 | + | "github.com/bnema/wlturbo" | |||
| 14 | + | "github.com/bnema/wlturbo/protocol/core" | |||
| 15 | + | "github.com/bnema/wlturbo/protocol/drmsyncobj" | |||
| 16 | + | "github.com/bnema/wlturbo/protocol/fractionalscale" | |||
| 17 | + | "github.com/bnema/wlturbo/protocol/linuxdmabuf" | |||
| 18 | + | "github.com/bnema/wlturbo/protocol/textinput" | |||
| 19 | + | "github.com/bnema/wlturbo/protocol/viewporter" | |||
| 20 | + | "github.com/bnema/wlturbo/protocol/xdgshell" | |||
| 21 | + | "golang.org/x/sys/unix" | |||
| 22 | + | ) | |||
| 23 | + | ||||
| 24 | + | func pair(t *testing.T) (*net.UnixConn, *net.UnixConn) { | |||
| 25 | + | t.Helper() | |||
| 26 | + | fds, e := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0) | |||
| 27 | + | if e != nil { | |||
| 28 | + | t.Fatal(e) | |||
| 29 | + | } | |||
| 30 | + | a := os.NewFile(uintptr(fds[0]), "a") | |||
| 31 | + | b := os.NewFile(uintptr(fds[1]), "b") | |||
| 32 | + | ca, e := net.FileConn(a) | |||
| 33 | + | a.Close() | |||
| 34 | + | if e != nil { | |||
| 35 | + | t.Fatal(e) | |||
| 36 | + | } | |||
| 37 | + | cb, e := net.FileConn(b) | |||
| 38 | + | b.Close() | |||
| 39 | + | if e != nil { | |||
| 40 | + | t.Fatal(e) | |||
| 41 | + | } | |||
| 42 | + | t.Cleanup(func() { ca.Close(); cb.Close() }) | |||
| 43 | + | return ca.(*net.UnixConn), cb.(*net.UnixConn) | |||
| 44 | + | } | |||
| 45 | + | func msg(id uint32, op uint16, body ...uint32) []byte { | |||
| 46 | + | b := make([]byte, 8+4*len(body)) | |||
| 47 | + | binary.NativeEndian.PutUint32(b, id) | |||
| 48 | + | binary.NativeEndian.PutUint32(b[4:], uint32(len(b))<<16|uint32(op)) | |||
| 49 | + | for i, v := range body { | |||
| 50 | + | binary.NativeEndian.PutUint32(b[8+i*4:], v) | |||
| 51 | + | } | |||
| 52 | + | return b | |||
| 53 | + | } | |||
| 54 | + | func payload(id uint32, op uint16, b ...byte) []byte { | |||
| 55 | + | p := make([]byte, 8+len(b)) | |||
| 56 | + | copy(p, msg(id, op)) | |||
| 57 | + | copy(p[8:], b) | |||
| 58 | + | binary.NativeEndian.PutUint32(p[4:], uint32(len(p))<<16|uint32(op)) | |||
| 59 | + | return p | |||
| 60 | + | } | |||
| 61 | + | func arr(b []byte) []byte { | |||
| 62 | + | out := make([]byte, 4+(len(b)+3)&^3) | |||
| 63 | + | binary.NativeEndian.PutUint32(out, uint32(len(b))) | |||
| 64 | + | copy(out[4:], b) | |||
| 65 | + | return out | |||
| 66 | + | } | |||
| 67 | + | func str(s string) []byte { return arr(append([]byte(s), 0)) } | |||
| 68 | + | func send(t *testing.T, p *net.UnixConn, b []byte, fd ...int) { | |||
| 69 | + | t.Helper() | |||
| 70 | + | var rights []byte | |||
| 71 | + | if len(fd) > 0 { | |||
| 72 | + | rights = unix.UnixRights(fd...) | |||
| 73 | + | } | |||
| 74 | + | if _, _, e := p.WriteMsgUnix(b, rights, nil); e != nil { | |||
| 75 | + | t.Fatal(e) | |||
| 76 | + | } | |||
| 77 | + | } | |||
| 78 | + | func request(t *testing.T, p *net.UnixConn) (uint32, uint16, []byte) { | |||
| 79 | + | t.Helper() | |||
| 80 | + | p.SetReadDeadline(time.Now().Add(2 * time.Second)) | |||
| 81 | + | b := make([]byte, 4096) | |||
| 82 | + | n, e := p.Read(b) | |||
| 83 | + | if e != nil { | |||
| 84 | + | t.Fatal(e) | |||
| 85 | + | } | |||
| 86 | + | if n < 8 { | |||
| 87 | + | t.Fatal("short request") | |||
| 88 | + | } | |||
| 89 | + | return binary.NativeEndian.Uint32(b), uint16(binary.NativeEndian.Uint32(b[4:])), b[8:n] | |||
| 90 | + | } | |||
| 91 | + | func noerr(t *testing.T, e error) { | |||
| 92 | + | t.Helper() | |||
| 93 | + | if e != nil { | |||
| 94 | + | t.Fatal(e) | |||
| 95 | + | } | |||
| 96 | + | } | |||
| 97 | + | func bind(t *testing.T, d *wlturbo.Display, p *net.UnixConn, name uint32, iface string, supported uint32, proxy wlturbo.Proxy) { | |||
| 98 | + | t.Helper() | |||
| 99 | + | v := supported | |||
| 100 | + | if v > 4 { | |||
| 101 | + | v = 4 | |||
| 102 | + | } | |||
| 103 | + | noerr(t, d.Registry().Bind(name, iface, v, proxy)) | |||
| 104 | + | _, op, b := request(t, p) | |||
| 105 | + | if op != 0 || binary.NativeEndian.Uint32(b) != name || binary.NativeEndian.Uint32(b[len(b)-8:]) != v || binary.NativeEndian.Uint32(b[len(b)-4:]) != proxy.ID() { | |||
| 106 | + | t.Fatalf("bad bind %s version=%d %x", iface, v, b) | |||
| 107 | + | } | |||
| 108 | + | } | |||
| 109 | + | func TestExtensionsOverSocketpair(t *testing.T) { | |||
| 110 | + | c, p := pair(t) | |||
| 111 | + | d, e := wlturbo.ConnectFromConn(c) | |||
| 112 | + | noerr(t, e) | |||
| 113 | + | defer d.Close() | |||
| 114 | + | request(t, p) | |||
| 115 | + | // A registry announcement is the only input to optional-global discovery. | |||
| 116 | + | send(t, p, payload(d.Registry().ID(), 0, append(msg(0, 0, 11)[8:12], append(str(fractionalscale.WpFractionalScaleManagerInterface), msg(0, 0, 1)[8:12]...)...)...)) | |||
| 117 | + | noerr(t, d.Dispatch()) | |||
| 118 | + | if g, ok := d.Registry().FindGlobal(fractionalscale.WpFractionalScaleManagerInterface); !ok || g.Version != 1 { | |||
| 119 | + | t.Fatal("announced fractional scale missing", g, ok) | |||
| 120 | + | } | |||
| 121 | + | if _, ok := d.Registry().FindGlobal(textinput.TextInputManagerV3Interface); ok { | |||
| 122 | + | t.Fatal("absent text input") | |||
| 123 | + | } | |||
| 124 | + | surface := core.NewSurface(d.Context()) | |||
| 125 | + | surface.SetID(d.AllocateID()) | |||
| 126 | + | d.Context().Register(surface) | |||
| 127 | + | seat := core.NewSeat(d.Context()) | |||
| 128 | + | seat.SetID(d.AllocateID()) | |||
| 129 | + | d.Context().Register(seat) | |||
| 130 | + | wm := xdgshell.NewXdgWmBase(d.Context()) | |||
| 131 | + | bind(t, d, p, 1, xdgshell.XdgWmBaseInterface, 7, wm) | |||
| 132 | + | xs, e := wm.GetXdgSurface(surface) | |||
| 133 | + | noerr(t, e) | |||
| 134 | + | request(t, p) | |||
| 135 | + | top, e := xs.GetToplevel() | |||
| 136 | + | noerr(t, e) | |||
| 137 | + | request(t, p) | |||
| 138 | + | var sequence []string | |||
| 139 | + | top.OnConfigure(func(w, h int32, states []byte) { | |||
| 140 | + | if w != 800 || h != 600 { | |||
| 141 | + | t.Fatalf("size %d,%d", w, h) | |||
| 142 | + | } | |||
| 143 | + | sequence = append(sequence, "top") | |||
| 144 | + | }) | |||
| 145 | + | xs.OnConfigure(func(serial uint32) { sequence = append(sequence, "surface"); noerr(t, xs.AckConfigure(serial)) }) | |||
| 146 | + | send(t, p, payload(top.ID(), 0, append(msg(0, 0, 800, 600)[8:], arr(nil)...)...)) | |||
| 147 | + | noerr(t, d.Dispatch()) | |||
| 148 | + | send(t, p, msg(xs.ID(), 0, 9)) | |||
| 149 | + | noerr(t, d.Dispatch()) | |||
| 150 | + | _, op, b := request(t, p) | |||
| 151 | + | if op != 4 || binary.NativeEndian.Uint32(b) != 9 || strings.Join(sequence, ",") != "top,surface" { | |||
| 152 | + | t.Fatalf("xdg %d %x %v", op, b, sequence) | |||
| 153 | + | } | |||
| 154 | + | noerr(t, top.Destroy()) | |||
| 155 | + | request(t, p) | |||
| 156 | + | noerr(t, xs.Destroy()) | |||
| 157 | + | request(t, p) | |||
| 158 | + | noerr(t, wm.Destroy()) | |||
| 159 | + | request(t, p) | |||
| 160 | + | dm := linuxdmabuf.NewLinuxDmabuf(d.Context()) | |||
| 161 | + | bind(t, d, p, 2, linuxdmabuf.LinuxDmabufInterface, 6, dm) | |||
| 162 | + | fb, e := dm.GetDefaultFeedback() | |||
| 163 | + | noerr(t, e) | |||
| 164 | + | request(t, p) | |||
| 165 | + | table, e := os.CreateTemp(t.TempDir(), "table") | |||
| 166 | + | noerr(t, e) | |||
| 167 | + | var bytes [32]byte | |||
| 168 | + | binary.NativeEndian.PutUint32(bytes[:], 0x34325258) | |||
| 169 | + | binary.NativeEndian.PutUint64(bytes[8:], 19) | |||
| 170 | + | binary.NativeEndian.PutUint32(bytes[16:], 0x34325241) | |||
| 171 | + | binary.NativeEndian.PutUint64(bytes[24:], 42) | |||
| 172 | + | _, e = table.Write(bytes[:]) | |||
| 173 | + | noerr(t, e) | |||
| 174 | + | defer table.Close() | |||
| 175 | + | var entries []linuxdmabuf.FormatEntry | |||
| 176 | + | var indices []uint16 | |||
| 177 | + | var feedback []string | |||
| 178 | + | fb.OnMainDevice(func(dev []byte) { | |||
| 179 | + | if len(dev) != 8 { | |||
| 180 | + | t.Fatalf("device %x", dev) | |||
| 181 | + | } | |||
| 182 | + | feedback = append(feedback, "main") | |||
| 183 | + | }) | |||
| 184 | + | fb.OnFormatTable(func(fd *wlturbo.OwnedFD, size uint32) { | |||
| 185 | + | entries, e = linuxdmabuf.ReadFormatTable(fd, size) | |||
| 186 | + | noerr(t, e) | |||
| 187 | + | feedback = append(feedback, "table") | |||
| 188 | + | }) | |||
| 189 | + | fb.OnTrancheTargetDevice(func(dev []byte) { feedback = append(feedback, "target") }) | |||
| 190 | + | fb.OnTrancheFormats(func(data []byte) { | |||
| 191 | + | indices, e = linuxdmabuf.TrancheIndices(data, len(entries)) | |||
| 192 | + | noerr(t, e) | |||
| 193 | + | feedback = append(feedback, "formats") | |||
| 194 | + | }) | |||
| 195 | + | fb.OnTrancheFlags(func(flags uint32) { | |||
| 196 | + | if flags != 1 { | |||
| 197 | + | t.Fatal(flags) | |||
| 198 | + | } | |||
| 199 | + | feedback = append(feedback, "flags") | |||
| 200 | + | }) | |||
| 201 | + | fb.OnTrancheDone(func() { feedback = append(feedback, "tranche") }) | |||
| 202 | + | fb.OnDone(func() { feedback = append(feedback, "done") }) | |||
| 203 | + | send(t, p, payload(fb.ID(), 2, arr(make([]byte, 8))...)) | |||
| 204 | + | noerr(t, d.Dispatch()) | |||
| 205 | + | send(t, p, msg(fb.ID(), 1, 32), int(table.Fd())) | |||
| 206 | + | noerr(t, d.Dispatch()) | |||
| 207 | + | send(t, p, payload(fb.ID(), 4, arr(make([]byte, 8))...)) | |||
| 208 | + | noerr(t, d.Dispatch()) | |||
| 209 | + | send(t, p, payload(fb.ID(), 5, arr([]byte{0, 0, 1, 0})...)) | |||
| 210 | + | noerr(t, d.Dispatch()) | |||
| 211 | + | send(t, p, msg(fb.ID(), 6, 1)) | |||
| 212 | + | noerr(t, d.Dispatch()) | |||
| 213 | + | send(t, p, msg(fb.ID(), 3)) | |||
| 214 | + | noerr(t, d.Dispatch()) | |||
| 215 | + | send(t, p, msg(fb.ID(), 0)) | |||
| 216 | + | noerr(t, d.Dispatch()) | |||
| 217 | + | if len(entries) != 2 || entries[1].Modifier != 42 || len(indices) != 2 || indices[1] != 1 || strings.Join(feedback, ",") != "main,table,target,formats,flags,tranche,done" { | |||
| 218 | + | t.Fatalf("feedback %v %v %v", entries, indices, feedback) | |||
| 219 | + | } | |||
| 220 | + | noerr(t, fb.Destroy()) | |||
| 221 | + | request(t, p) | |||
| 222 | + | params, e := dm.CreateParams() | |||
| 223 | + | noerr(t, e) | |||
| 224 | + | request(t, p) | |||
| 225 | + | f, e := os.Open("/dev/null") | |||
| 226 | + | noerr(t, e) | |||
| 227 | + | noerr(t, params.Add(int(f.Fd()), 0, 0, 4, 0, 0)) | |||
| 228 | + | request(t, p) | |||
| 229 | + | if _, e := unix.FcntlInt(f.Fd(), unix.F_GETFD, 0); e == nil { | |||
| 230 | + | t.Fatal("sent fd not closed") | |||
| 231 | + | } | |||
| 232 | + | f.Close() | |||
| 233 | + | var buffer *core.Buffer | |||
| 234 | + | params.OnCreated(func(b *core.Buffer) { buffer = b }) | |||
| 235 | + | params.OnFailed(func() { sequence = append(sequence, "failed") }) | |||
| 236 | + | noerr(t, params.Create(1, 1, 0x34325258, 0)) | |||
| 237 | + | request(t, p) | |||
| 238 | + | send(t, p, msg(params.ID(), 0, 999)) | |||
| 239 | + | noerr(t, d.Dispatch()) | |||
| 240 | + | if buffer == nil || buffer.ID() != 999 { | |||
| 241 | + | t.Fatal("typed buffer missing") | |||
| 242 | + | } | |||
| 243 | + | noerr(t, buffer.Destroy()) | |||
| 244 | + | request(t, p) | |||
| 245 | + | noerr(t, params.Destroy()) | |||
| 246 | + | request(t, p) | |||
| 247 | + | failedParams, e := dm.CreateParams() | |||
| 248 | + | noerr(t, e) | |||
| 249 | + | request(t, p) | |||
| 250 | + | failedParams.OnFailed(func() { sequence = append(sequence, "failed") }) | |||
| 251 | + | noerr(t, failedParams.Create(1, 1, 0x34325258, 0)) | |||
| 252 | + | request(t, p) | |||
| 253 | + | send(t, p, msg(failedParams.ID(), 1)) | |||
| 254 | + | noerr(t, d.Dispatch()) | |||
| 255 | + | if sequence[len(sequence)-1] != "failed" { | |||
| 256 | + | t.Fatal(sequence) | |||
| 257 | + | } | |||
| 258 | + | noerr(t, failedParams.Destroy()) | |||
| 259 | + | request(t, p) | |||
| 260 | + | immedParams, e := dm.CreateParams() | |||
| 261 | + | noerr(t, e) | |||
| 262 | + | request(t, p) | |||
| 263 | + | immediate, e := immedParams.CreateImmed(1, 1, 0x34325258, 0) | |||
| 264 | + | noerr(t, e) | |||
| 265 | + | request(t, p) | |||
| 266 | + | noerr(t, immediate.Destroy()) | |||
| 267 | + | request(t, p) | |||
| 268 | + | noerr(t, immedParams.Destroy()) | |||
| 269 | + | request(t, p) | |||
| 270 | + | noerr(t, dm.Destroy()) | |||
| 271 | + | request(t, p) | |||
| 272 | + | sync := drmsyncobj.NewWpLinuxDrmSyncobjManager(d.Context()) | |||
| 273 | + | bind(t, d, p, 3, drmsyncobj.WpLinuxDrmSyncobjManagerInterface, 1, sync) | |||
| 274 | + | ss, e := sync.GetSurface(surface) | |||
| 275 | + | noerr(t, e) | |||
| 276 | + | request(t, p) | |||
| 277 | + | f, e = os.Open("/dev/null") | |||
| 278 | + | noerr(t, e) | |||
| 279 | + | tl, e := sync.ImportTimeline(int(f.Fd())) | |||
| 280 | + | noerr(t, e) | |||
| 281 | + | request(t, p) | |||
| 282 | + | if _, e := unix.FcntlInt(f.Fd(), unix.F_GETFD, 0); e == nil { | |||
| 283 | + | t.Fatal("timeline fd not closed") | |||
| 284 | + | } | |||
| 285 | + | f.Close() | |||
| 286 | + | noerr(t, ss.SetAcquirePoint(tl, 0x123, 0x456)) | |||
| 287 | + | _, op, b = request(t, p) | |||
| 288 | + | if op != 1 || binary.NativeEndian.Uint32(b[4:]) != 0x123 || binary.NativeEndian.Uint32(b[8:]) != 0x456 { | |||
| 289 | + | t.Fatalf("acquire %d %x", op, b) | |||
| 290 | + | } | |||
| 291 | + | noerr(t, ss.SetReleasePoint(tl, 0x789, 0xabc)) | |||
| 292 | + | _, op, b = request(t, p) | |||
| 293 | + | if op != 2 || binary.NativeEndian.Uint32(b[4:]) != 0x789 { | |||
| 294 | + | t.Fatalf("release %d %x", op, b) | |||
| 295 | + | } | |||
| 296 | + | noerr(t, ss.Destroy()) | |||
| 297 | + | request(t, p) | |||
| 298 | + | noerr(t, tl.Destroy()) | |||
| 299 | + | request(t, p) | |||
| 300 | + | noerr(t, sync.Destroy()) | |||
| 301 | + | request(t, p) | |||
| 302 | + | vp := viewporter.NewWpViewporter(d.Context()) | |||
| 303 | + | bind(t, d, p, 4, viewporter.WpViewporterInterface, 1, vp) | |||
| 304 | + | view, e := vp.GetViewport(surface) | |||
| 305 | + | noerr(t, e) | |||
| 306 | + | request(t, p) | |||
| 307 | + | noerr(t, view.Destroy()) | |||
| 308 | + | request(t, p) | |||
| 309 | + | noerr(t, vp.Destroy()) | |||
| 310 | + | request(t, p) | |||
| 311 | + | fs := fractionalscale.NewWpFractionalScaleManager(d.Context()) | |||
| 312 | + | bind(t, d, p, 5, fractionalscale.WpFractionalScaleManagerInterface, 1, fs) | |||
| 313 | + | scale, e := fs.GetFractionalScale(surface) | |||
| 314 | + | noerr(t, e) | |||
| 315 | + | request(t, p) | |||
| 316 | + | var preferred uint32 | |||
| 317 | + | scale.OnPreferredScale(func(v uint32) { preferred = v }) | |||
| 318 | + | send(t, p, msg(scale.ID(), 0, 150)) | |||
| 319 | + | noerr(t, d.Dispatch()) | |||
| 320 | + | if preferred != 150 { | |||
| 321 | + | t.Fatal(preferred) | |||
| 322 | + | } | |||
| 323 | + | noerr(t, scale.Destroy()) | |||
| 324 | + | request(t, p) | |||
| 325 | + | noerr(t, fs.Destroy()) | |||
| 326 | + | request(t, p) | |||
| 327 | + | ti := textinput.NewTextInputManagerV3(d.Context()) | |||
| 328 | + | bind(t, d, p, 6, textinput.TextInputManagerV3Interface, 1, ti) | |||
| 329 | + | input, e := ti.GetTextInput(seat) | |||
| 330 | + | noerr(t, e) | |||
| 331 | + | request(t, p) | |||
| 332 | + | noerr(t, input.Enable()) | |||
| 333 | + | request(t, p) | |||
| 334 | + | noerr(t, input.SetSurroundingText("hello", 5, 5)) | |||
| 335 | + | request(t, p) | |||
| 336 | + | noerr(t, input.Commit()) | |||
| 337 | + | request(t, p) | |||
| 338 | + | var events []string | |||
| 339 | + | input.OnPreeditString(func(s string, _, _ int32) { events = append(events, "preedit:"+s) }) | |||
| 340 | + | input.OnCommitString(func(s string) { events = append(events, "commit:"+s) }) | |||
| 341 | + | input.OnDeleteSurroundingText(func(_, _ uint32) { events = append(events, "delete") }) | |||
| 342 | + | input.OnDone(func(uint32) { events = append(events, "done") }) | |||
| 343 | + | send(t, p, payload(input.ID(), 2, append(str("é"), msg(0, 0, 0, 0)[8:]...)...)) | |||
| 344 | + | noerr(t, d.Dispatch()) | |||
| 345 | + | send(t, p, payload(input.ID(), 3, str("hi")...)) | |||
| 346 | + | noerr(t, d.Dispatch()) | |||
| 347 | + | send(t, p, msg(input.ID(), 4, 1, 2)) | |||
| 348 | + | noerr(t, d.Dispatch()) | |||
| 349 | + | send(t, p, msg(input.ID(), 5, 1)) | |||
| 350 | + | noerr(t, d.Dispatch()) | |||
| 351 | + | if strings.Join(events, ",") != "preedit:é,commit:hi,delete,done" { | |||
| 352 | + | t.Fatal(events) | |||
| 353 | + | } | |||
| 354 | + | noerr(t, input.Disable()) | |||
| 355 | + | request(t, p) | |||
| 356 | + | noerr(t, input.Commit()) | |||
| 357 | + | request(t, p) | |||
| 358 | + | noerr(t, input.Destroy()) | |||
| 359 | + | request(t, p) | |||
| 360 | + | noerr(t, ti.Destroy()) | |||
| 361 | + | request(t, p) | |||
| 362 | + | // Core data-device remains in protocol/core; verify its typed child and | |||
| 363 | + | // both directions of clipboard descriptor ownership. | |||
| 364 | + | mgr := core.NewDataDeviceManager(d.Context()) | |||
| 365 | + | bind(t, d, p, 7, core.DataDeviceManagerInterface, 3, mgr) | |||
| 366 | + | device, e := mgr.GetDataDevice(seat) | |||
| 367 | + | noerr(t, e) | |||
| 368 | + | request(t, p) | |||
| 369 | + | source, e := mgr.CreateDataSource() | |||
| 370 | + | noerr(t, e) | |||
| 371 | + | request(t, p) | |||
| 372 | + | var offer *core.DataOffer | |||
| 373 | + | device.OnDataOffer(func(o *core.DataOffer) { offer = o }) | |||
| 374 | + | send(t, p, msg(device.ID(), 0, 998)) | |||
| 375 | + | noerr(t, d.Dispatch()) | |||
| 376 | + | if offer == nil || offer.ID() != 998 { | |||
| 377 | + | t.Fatal("offer not typed") | |||
| 378 | + | } | |||
| 379 | + | read, write, e := os.Pipe() | |||
| 380 | + | noerr(t, e) | |||
| 381 | + | defer read.Close() | |||
| 382 | + | noerr(t, offer.Receive("text/plain", int(write.Fd()))) | |||
| 383 | + | request(t, p) | |||
| 384 | + | if _, e := unix.FcntlInt(write.Fd(), unix.F_GETFD, 0); e == nil { | |||
| 385 | + | t.Fatal("clipboard sent fd retained") | |||
| 386 | + | } | |||
| 387 | + | write.Close() | |||
| 388 | + | source.OnSend(func(mime string, fd *wlturbo.OwnedFD) { | |||
| 389 | + | if mime != "text/plain" { | |||
| 390 | + | t.Fatal(mime) | |||
| 391 | + | } | |||
| 392 | + | n, e := fd.Take() | |||
| 393 | + | noerr(t, e) | |||
| 394 | + | unix.Close(n) | |||
| 395 | + | }) | |||
| 396 | + | received, e := os.Open("/dev/null") | |||
| 397 | + | noerr(t, e) | |||
| 398 | + | send(t, p, payload(source.ID(), 1, str("text/plain")...), int(received.Fd())) | |||
| 399 | + | noerr(t, d.Dispatch()) | |||
| 400 | + | received.Close() | |||
| 401 | + | noerr(t, offer.Destroy()) | |||
| 402 | + | request(t, p) | |||
| 403 | + | noerr(t, source.Destroy()) | |||
| 404 | + | request(t, p) | |||
| 405 | + | noerr(t, device.Release()) | |||
| 406 | + | request(t, p) | |||
| 407 | + | noerr(t, mgr.Release()) | |||
| 408 | + | request(t, p) | |||
| 409 | + | ||||
| 410 | + | } | |||
| 411 | + | ||||
| 412 | + | func TestLiveFeedback(t *testing.T) { | |||
| 413 | + | if os.Getenv("WLTURBO_LIVE") != "1" { | |||
| 414 | + | t.Skip("set WLTURBO_LIVE=1") | |||
| 415 | + | } | |||
| 416 | + | d, e := wlturbo.Connect("") | |||
| 417 | + | noerr(t, e) | |||
| 418 | + | defer d.Close() | |||
| 419 | + | noerr(t, d.Roundtrip()) | |||
| 420 | + | for _, iface := range []string{xdgshell.XdgWmBaseInterface, linuxdmabuf.LinuxDmabufInterface, drmsyncobj.WpLinuxDrmSyncobjManagerInterface, viewporter.WpViewporterInterface, fractionalscale.WpFractionalScaleManagerInterface, textinput.TextInputManagerV3Interface} { | |||
| 421 | + | g, ok := d.Registry().FindGlobal(iface) | |||
| 422 | + | if !ok { | |||
| 423 | + | t.Logf("%s absent", iface) | |||
| 424 | + | continue | |||
| 425 | + | } | |||
| 426 | + | v := g.Version | |||
| 427 | + | cap := uint32(1) | |||
| 428 | + | if iface == xdgshell.XdgWmBaseInterface { | |||
| 429 | + | cap = 7 | |||
| 430 | + | } | |||
| 431 | + | if iface == linuxdmabuf.LinuxDmabufInterface { | |||
| 432 | + | cap = 4 | |||
| 433 | + | } | |||
| 434 | + | if v > cap { | |||
| 435 | + | v = cap | |||
| 436 | + | } | |||
| 437 | + | t.Logf("%s negotiated=%d (server=%d)", iface, v, g.Version) | |||
| 438 | + | if iface != linuxdmabuf.LinuxDmabufInterface { | |||
| 439 | + | continue | |||
| 440 | + | } | |||
| 441 | + | if v < 4 { | |||
| 442 | + | t.Fatal("dmabuf feedback requires v4") | |||
| 443 | + | } | |||
| 444 | + | dm := linuxdmabuf.NewLinuxDmabuf(d.Context()) | |||
| 445 | + | noerr(t, d.Registry().Bind(g.Name, iface, v, dm)) | |||
| 446 | + | fb, e := dm.GetDefaultFeedback() | |||
| 447 | + | noerr(t, e) | |||
| 448 | + | done := false | |||
| 449 | + | fb.OnMainDevice(func(b []byte) { t.Logf("main_device=%x", b) }) | |||
| 450 | + | fb.OnFormatTable(func(fd *wlturbo.OwnedFD, size uint32) { | |||
| 451 | + | entries, err := linuxdmabuf.ReadFormatTable(fd, size) | |||
| 452 | + | if err != nil { | |||
| 453 | + | t.Errorf("table: %v", err) | |||
| 454 | + | } | |||
| 455 | + | t.Logf("format-table entries=%d", len(entries)) | |||
| 456 | + | }) | |||
| 457 | + | fb.OnDone(func() { done = true }) | |||
| 458 | + | for i := 0; i < 100 && !done; i++ { | |||
| 459 | + | noerr(t, d.Dispatch()) | |||
| 460 | + | } | |||
| 461 | + | if !done { | |||
| 462 | + | t.Fatal("no feedback done") | |||
| 463 | + | } | |||
| 464 | + | noerr(t, fb.Destroy()) | |||
| 465 | + | noerr(t, dm.Destroy()) | |||
| 466 | + | } | |||
| 467 | + | } | |||
A protocol/linuxdmabuf/table.go +68 -0
| @@ -0,0 +1,68 @@ | |||
| 1 | + | package linuxdmabuf | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "encoding/binary" | |
| 5 | + | "fmt" | |
| 6 | + | "io" | |
| 7 | + | ||
| 8 | + | "github.com/bnema/wlturbo/wl" | |
| 9 | + | "golang.org/x/sys/unix" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | // FormatEntry is one 16-byte format-table record. Padding is ignored. | |
| 13 | + | type FormatEntry struct { | |
| 14 | + | Format uint32 | |
| 15 | + | Modifier uint64 | |
| 16 | + | } | |
| 17 | + | ||
| 18 | + | // ReadFormatTable consumes an owned format_table descriptor, even on error. | |
| 19 | + | // It reads a bounded snapshot with pread rather than mapping compositor-owned | |
| 20 | + | // memory, avoiding SIGBUS if the backing file is unexpectedly truncated. | |
| 21 | + | func ReadFormatTable(fd *wl.OwnedFD, size uint32) ([]FormatEntry, error) { | |
| 22 | + | if fd == nil { | |
| 23 | + | return nil, fmt.Errorf("missing format table descriptor") | |
| 24 | + | } | |
| 25 | + | n, err := fd.Take() | |
| 26 | + | if err != nil { | |
| 27 | + | return nil, err | |
| 28 | + | } | |
| 29 | + | defer unix.Close(n) | |
| 30 | + | if size%16 != 0 || size > 16<<20 { | |
| 31 | + | return nil, fmt.Errorf("invalid format table size %d", size) | |
| 32 | + | } | |
| 33 | + | b := make([]byte, size) | |
| 34 | + | for off := 0; off < len(b); { | |
| 35 | + | read, e := unix.Pread(n, b[off:], int64(off)) | |
| 36 | + | off += read | |
| 37 | + | if e == unix.EINTR { | |
| 38 | + | continue | |
| 39 | + | } | |
| 40 | + | if e != nil { | |
| 41 | + | return nil, e | |
| 42 | + | } | |
| 43 | + | if read == 0 { | |
| 44 | + | return nil, io.ErrUnexpectedEOF | |
| 45 | + | } | |
| 46 | + | } | |
| 47 | + | entries := make([]FormatEntry, size/16) | |
| 48 | + | for i := range entries { | |
| 49 | + | off := i * 16 | |
| 50 | + | entries[i] = FormatEntry{binary.NativeEndian.Uint32(b[off:]), binary.NativeEndian.Uint64(b[off+8:])} | |
| 51 | + | } | |
| 52 | + | return entries, nil | |
| 53 | + | } | |
| 54 | + | ||
| 55 | + | // TrancheIndices decodes the uint16 indices into a format table. | |
| 56 | + | func TrancheIndices(data []byte, count int) ([]uint16, error) { | |
| 57 | + | if len(data)%2 != 0 { | |
| 58 | + | return nil, fmt.Errorf("odd tranche indices length") | |
| 59 | + | } | |
| 60 | + | indices := make([]uint16, len(data)/2) | |
| 61 | + | for i := range indices { | |
| 62 | + | indices[i] = binary.NativeEndian.Uint16(data[i*2:]) | |
| 63 | + | if int(indices[i]) >= count { | |
| 64 | + | return nil, fmt.Errorf("format table index %d out of range", indices[i]) | |
| 65 | + | } | |
| 66 | + | } | |
| 67 | + | return indices, nil | |
| 68 | + | } | |
| @@ -0,0 +1,68 @@ | |||||
| 1 | + | package linuxdmabuf | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "encoding/binary" | |||
| 5 | + | "fmt" | |||
| 6 | + | "io" | |||
| 7 | + | ||||
| 8 | + | "github.com/bnema/wlturbo/wl" | |||
| 9 | + | "golang.org/x/sys/unix" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | // FormatEntry is one 16-byte format-table record. Padding is ignored. | |||
| 13 | + | type FormatEntry struct { | |||
| 14 | + | Format uint32 | |||
| 15 | + | Modifier uint64 | |||
| 16 | + | } | |||
| 17 | + | ||||
| 18 | + | // ReadFormatTable consumes an owned format_table descriptor, even on error. | |||
| 19 | + | // It reads a bounded snapshot with pread rather than mapping compositor-owned | |||
| 20 | + | // memory, avoiding SIGBUS if the backing file is unexpectedly truncated. | |||
| 21 | + | func ReadFormatTable(fd *wl.OwnedFD, size uint32) ([]FormatEntry, error) { | |||
| 22 | + | if fd == nil { | |||
| 23 | + | return nil, fmt.Errorf("missing format table descriptor") | |||
| 24 | + | } | |||
| 25 | + | n, err := fd.Take() | |||
| 26 | + | if err != nil { | |||
| 27 | + | return nil, err | |||
| 28 | + | } | |||
| 29 | + | defer unix.Close(n) | |||
| 30 | + | if size%16 != 0 || size > 16<<20 { | |||
| 31 | + | return nil, fmt.Errorf("invalid format table size %d", size) | |||
| 32 | + | } | |||
| 33 | + | b := make([]byte, size) | |||
| 34 | + | for off := 0; off < len(b); { | |||
| 35 | + | read, e := unix.Pread(n, b[off:], int64(off)) | |||
| 36 | + | off += read | |||
| 37 | + | if e == unix.EINTR { | |||
| 38 | + | continue | |||
| 39 | + | } | |||
| 40 | + | if e != nil { | |||
| 41 | + | return nil, e | |||
| 42 | + | } | |||
| 43 | + | if read == 0 { | |||
| 44 | + | return nil, io.ErrUnexpectedEOF | |||
| 45 | + | } | |||
| 46 | + | } | |||
| 47 | + | entries := make([]FormatEntry, size/16) | |||
| 48 | + | for i := range entries { | |||
| 49 | + | off := i * 16 | |||
| 50 | + | entries[i] = FormatEntry{binary.NativeEndian.Uint32(b[off:]), binary.NativeEndian.Uint64(b[off+8:])} | |||
| 51 | + | } | |||
| 52 | + | return entries, nil | |||
| 53 | + | } | |||
| 54 | + | ||||
| 55 | + | // TrancheIndices decodes the uint16 indices into a format table. | |||
| 56 | + | func TrancheIndices(data []byte, count int) ([]uint16, error) { | |||
| 57 | + | if len(data)%2 != 0 { | |||
| 58 | + | return nil, fmt.Errorf("odd tranche indices length") | |||
| 59 | + | } | |||
| 60 | + | indices := make([]uint16, len(data)/2) | |||
| 61 | + | for i := range indices { | |||
| 62 | + | indices[i] = binary.NativeEndian.Uint16(data[i*2:]) | |||
| 63 | + | if int(indices[i]) >= count { | |||
| 64 | + | return nil, fmt.Errorf("format table index %d out of range", indices[i]) | |||
| 65 | + | } | |||
| 66 | + | } | |||
| 67 | + | return indices, nil | |||
| 68 | + | } | |||
A protocol/linuxdmabuf/table_test.go +28 -0
| @@ -0,0 +1,28 @@ | |||
| 1 | + | //go:build linux | |
| 2 | + | ||
| 3 | + | package linuxdmabuf_test | |
| 4 | + | ||
| 5 | + | import ( | |
| 6 | + | "encoding/binary" | |
| 7 | + | "github.com/bnema/wlturbo/protocol/linuxdmabuf" | |
| 8 | + | "testing" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | func TestTrancheIndices(t *testing.T) { | |
| 12 | + | b := make([]byte, 4) | |
| 13 | + | binary.NativeEndian.PutUint16(b, 1) | |
| 14 | + | binary.NativeEndian.PutUint16(b[2:], 0) | |
| 15 | + | got, e := linuxdmabuf.TrancheIndices(b, 2) | |
| 16 | + | if e != nil || len(got) != 2 || got[0] != 1 || got[1] != 0 { | |
| 17 | + | t.Fatalf("indices %v %v", got, e) | |
| 18 | + | } | |
| 19 | + | if _, e = linuxdmabuf.TrancheIndices(b, 1); e == nil { | |
| 20 | + | t.Fatal("out of range accepted") | |
| 21 | + | } | |
| 22 | + | if _, e = linuxdmabuf.TrancheIndices(b[:3], 2); e == nil { | |
| 23 | + | t.Fatal("odd length accepted") | |
| 24 | + | } | |
| 25 | + | if _, e = linuxdmabuf.ReadFormatTable(nil, 0); e == nil { | |
| 26 | + | t.Fatal("missing fd accepted") | |
| 27 | + | } | |
| 28 | + | } | |
| @@ -0,0 +1,28 @@ | |||||
| 1 | + | //go:build linux | |||
| 2 | + | ||||
| 3 | + | package linuxdmabuf_test | |||
| 4 | + | ||||
| 5 | + | import ( | |||
| 6 | + | "encoding/binary" | |||
| 7 | + | "github.com/bnema/wlturbo/protocol/linuxdmabuf" | |||
| 8 | + | "testing" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | func TestTrancheIndices(t *testing.T) { | |||
| 12 | + | b := make([]byte, 4) | |||
| 13 | + | binary.NativeEndian.PutUint16(b, 1) | |||
| 14 | + | binary.NativeEndian.PutUint16(b[2:], 0) | |||
| 15 | + | got, e := linuxdmabuf.TrancheIndices(b, 2) | |||
| 16 | + | if e != nil || len(got) != 2 || got[0] != 1 || got[1] != 0 { | |||
| 17 | + | t.Fatalf("indices %v %v", got, e) | |||
| 18 | + | } | |||
| 19 | + | if _, e = linuxdmabuf.TrancheIndices(b, 1); e == nil { | |||
| 20 | + | t.Fatal("out of range accepted") | |||
| 21 | + | } | |||
| 22 | + | if _, e = linuxdmabuf.TrancheIndices(b[:3], 2); e == nil { | |||
| 23 | + | t.Fatal("odd length accepted") | |||
| 24 | + | } | |||
| 25 | + | if _, e = linuxdmabuf.ReadFormatTable(nil, 0); e == nil { | |||
| 26 | + | t.Fatal("missing fd accepted") | |||
| 27 | + | } | |||
| 28 | + | } | |||