session_lock_surface_test.go RawHistoryFolder

1package wayland
2
3import (
4	"testing"
5	"time"
6
7	"github.com/bnema/go-wayland-bindings/server/extsessionlock"
8	"github.com/bnema/go-wayland-bindings/server/linuxdrmsyncobj"
9	"github.com/bnema/go-wayland-bindings/server/viewporter"
10	"github.com/bnema/go-wayland-bindings/server/wayland"
11	"github.com/bnema/go-wayland-bindings/server/wlrlayershell"
12	"github.com/bnema/go-wayland-bindings/server/xdgshell"
13	"github.com/bnema/wlturbo"
14	"golang.org/x/sys/unix"
15)
16
17// This is a real wire proxy, not a double for a project interface.
18type lockConfigureProxy struct {
19	wlturbo.BaseProxy
20	configures chan lockConfigure
21}
22
23func (p *lockConfigureProxy) Dispatch(e *wlturbo.Event) {
24	if e.Opcode == uint16(extsessionlock.ExtSessionLockSurfaceV1EventConfigure) {
25		p.configures <- lockConfigure{serial: e.Uint32(), width: int(e.Uint32()), height: int(e.Uint32())}
26	}
27}
28
29func lockTestSurface(t *testing.T, s *Server, c *wlturbo.Display) (uint32, uint32, uint32, *surface, *lockSurface, <-chan lockConfigure) {
30	t.Helper()
31	comp := bindVersion(t, c, "wl_compositor", 6)
32	surfID := c.AllocateID()
33	requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surfID)
34	registerProtocol(t, c, surfID)
35	if err := c.Roundtrip(); err != nil {
36		t.Fatal(err)
37	}
38	ownerID, lockID := c.AllocateID(), c.AllocateID()
39	registerProtocol(t, c, ownerID)
40	setWireSchema(c, ownerID, "ext_session_lock_v1", 1)
41	setWireSchema(c, lockID, "ext_session_lock_surface_v1", 1)
42	configures := make(chan lockConfigure, 8)
43	p := &lockConfigureProxy{configures: configures}
44	p.SetID(lockID)
45	registerWireProxy(c, p)
46	var surf *surface
47	var l *lockSurface
48	s.display.Do(func() {
49		for _, v := range s.surfaces {
50			if v.wl.ID() == surfID {
51				surf = v
52			}
53		}
54		if surf == nil {
55			t.Error("surface missing")
56			return
57		}
58		// The role factory takes an already created owner resource. No manager
59		// or test global is needed and no admission behavior is simulated.
60		r, err := surf.wl.Client().CreateResource(extsessionlock.ExtSessionLockV1Interface, 1, ownerID, nil)
61		if err != nil {
62			t.Error(err)
63			return
64		}
65		l, err = s.newLockSurface(extsessionlock.WrapExtSessionLockV1(r), lockID, surf, s.outputs[0], nil)
66		if err != nil {
67			t.Error(err)
68		}
69	})
70	if l == nil {
71		t.Fatal("lock role missing")
72	}
73	if err := c.Roundtrip(); err != nil {
74		t.Fatal(err)
75	}
76	return surfID, ownerID, lockID, surf, l, configures
77}
78
79func lockViewport(t *testing.T, c *wlturbo.Display, surfID uint32, w, h int) uint32 {
80	t.Helper()
81	manager := bindProtocol(t, c, "wp_viewporter")
82	vp := c.AllocateID()
83	registerProtocol(t, c, vp)
84	requestProtocol(t, c, manager, viewporter.WpViewporterRequestGetViewport, vp, surfID)
85	requestProtocol(t, c, vp, viewporter.WpViewportRequestSetDestination, int32(w), int32(h))
86	return vp
87}
88
89func TestLockSurfaceInitialConfigureAndCommitErrors(t *testing.T) {
90	for _, scenario := range []string{"before ack", "queued before ack", "null", "dimensions", "unknown ack", "reused ack", "old ack"} {
91		t.Run(scenario, func(t *testing.T) {
92			s, _, _, dir := lifecycleServer(t)
93			c := protocolClient(t, s, dir)
94			surfID, _, lockID, surf, l, configures := lockTestSurface(t, s, c)
95			cfg := <-configures
96			if cfg.width != testOutputs[0].Width || cfg.height != testOutputs[0].Height || cfg.serial == 0 {
97				t.Fatalf("configure = %+v", cfg)
98			}
99			code := extsessionlock.ExtSessionLockSurfaceV1ErrorInvalidSerial
100			switch scenario {
101			case "before ack", "queued before ack":
102				if scenario == "queued before ack" {
103					s.display.Do(func() { surf.barrier = true; surf.next.wait = true })
104				}
105				// Hold the display goroutine while both requests are written: the
106				// server reads neither before the release, so the fatal commit
107				// cannot close the connection under the ACK write.
108				entered, release := make(chan struct{}), make(chan struct{})
109				go s.display.Do(func() { close(entered); <-release })
110				<-entered
111				requestProtocol(t, c, surfID, wayland.SurfaceRequestCommit)
112				// This later ACK must not legalize an earlier FIFO commit.
113				requestProtocol(t, c, lockID, extsessionlock.ExtSessionLockSurfaceV1RequestAckConfigure, cfg.serial)
114				close(release)
115				code = extsessionlock.ExtSessionLockSurfaceV1ErrorCommitBeforeFirstAck
116			case "null":
117				requestProtocol(t, c, lockID, extsessionlock.ExtSessionLockSurfaceV1RequestAckConfigure, cfg.serial)
118				requestProtocol(t, c, surfID, wayland.SurfaceRequestCommit)
119				code = extsessionlock.ExtSessionLockSurfaceV1ErrorNullBuffer
120			case "dimensions":
121				requestProtocol(t, c, lockID, extsessionlock.ExtSessionLockSurfaceV1RequestAckConfigure, cfg.serial)
122				requestProtocol(t, c, surfID, wayland.SurfaceRequestAttach, shmBuffer(t, c), int32(0), int32(0))
123				requestProtocol(t, c, surfID, wayland.SurfaceRequestCommit)
124				code = extsessionlock.ExtSessionLockSurfaceV1ErrorDimensionsMismatch
125			case "unknown ack":
126				requestProtocol(t, c, lockID, extsessionlock.ExtSessionLockSurfaceV1RequestAckConfigure, uint32(0))
127			case "reused ack":
128				requestProtocol(t, c, lockID, extsessionlock.ExtSessionLockSurfaceV1RequestAckConfigure, cfg.serial)
129				requestProtocol(t, c, lockID, extsessionlock.ExtSessionLockSurfaceV1RequestAckConfigure, cfg.serial)
130			case "old ack":
131				s.display.Do(l.sendConfigure)
132				if err := c.Roundtrip(); err != nil {
133					t.Fatal(err)
134				}
135				newer := <-configures
136				requestProtocol(t, c, lockID, extsessionlock.ExtSessionLockSurfaceV1RequestAckConfigure, newer.serial)
137				requestProtocol(t, c, lockID, extsessionlock.ExtSessionLockSurfaceV1RequestAckConfigure, cfg.serial)
138			}
139			expectProtocolError(t, c, lockID, uint32(code))
140		})
141	}
142}
143
144func TestLockSurfaceCapturedAckGeometryAndDestroy(t *testing.T) {
145	s, _, _, contents, dir := contentServer(t)
146	c := protocolClient(t, s, dir)
147	surfID, _, lockID, surf, l, configures := lockTestSurface(t, s, c)
148	cfg := <-configures
149	vpID := lockViewport(t, c, surfID, cfg.width, cfg.height)
150	requestProtocol(t, c, lockID, extsessionlock.ExtSessionLockSurfaceV1RequestAckConfigure, cfg.serial)
151	requestProtocol(t, c, surfID, wayland.SurfaceRequestAttach, shmBuffer(t, c), int32(0), int32(0))
152	if err := c.Roundtrip(); err != nil {
153		t.Fatal(err)
154	}
155	// Capture a real buffer and geometry behind a future commit-time gate.
156	s.display.Do(func() {
157		surf.next.at = time.Now().Add(time.Hour)
158		surf.Commit(surf.wl)
159		if len(surf.queue) != 1 || surf.queue[0].lockAck != cfg {
160			t.Errorf("captured ACK missing")
161		}
162		l.output.place.Width++
163		l.sendConfigure()
164	})
165	if err := c.Roundtrip(); err != nil {
166		t.Fatal(err)
167	}
168	newer := <-configures
169	requestProtocol(t, c, lockID, extsessionlock.ExtSessionLockSurfaceV1RequestAckConfigure, newer.serial)
170	requestProtocol(t, c, vpID, viewporter.WpViewportRequestSetDestination, int32(newer.width), int32(newer.height))
171	if err := c.Roundtrip(); err != nil {
172		t.Fatal(err)
173	}
174	s.display.Do(func() {
175		u := surf.queue[0]
176		u.at = time.Time{}
177		u.applyGraph()
178		if !l.mapped || surf.committedViewport.destW != int32(cfg.width) || l.ack != newer {
179			t.Errorf("captured commit read live geometry/ACK")
180		}
181		// Queue another update and destroy: retirement must clear all holds.
182		surf.next.at = time.Now().Add(time.Hour)
183		surf.Commit(surf.wl)
184	})
185	requestProtocol(t, c, lockID, extsessionlock.ExtSessionLockSurfaceV1RequestDestroy)
186	if err := c.Roundtrip(); err != nil {
187		t.Fatal(err)
188	}
189	s.display.Do(func() {
190		if surf.kind != roleSessionLock || surf.lock != nil || len(surf.queue) != 0 || surf.current != nil || len(s.lockSurfaces) != 0 || !l.closed {
191			t.Error("destroy lost permanent role or retained queued content")
192		}
193	})
194	select {
195	case content := <-contents:
196		if !content.Empty() {
197			select {
198			case content = <-contents:
199				if !content.Empty() {
200					t.Error("destroy did not emit empty content")
201				}
202			case <-time.After(time.Second):
203				t.Fatal("no empty content")
204			}
205		}
206	case <-time.After(time.Second):
207		t.Fatal("no content emitted")
208	}
209	wm := bindProtocol(t, c, "xdg_wm_base")
210	requestProtocol(t, c, wm, xdgshell.WmBaseRequestGetXdgSurface, c.AllocateID(), surfID)
211	expectProtocolError(t, c, wm, uint32(xdgshell.WmBaseErrorRole))
212}
213
214func TestLockSurfaceRejectsHistoricalBuffer(t *testing.T) {
215	s, _, _, dir := lifecycleServer(t)
216	c := protocolClient(t, s, dir)
217	comp := bindProtocol(t, c, "wl_compositor")
218	surfID := c.AllocateID()
219	requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surfID)
220	registerProtocol(t, c, surfID)
221	requestProtocol(t, c, surfID, wayland.SurfaceRequestAttach, shmBuffer(t, c), int32(0), int32(0))
222	requestProtocol(t, c, surfID, wayland.SurfaceRequestCommit)
223	requestProtocol(t, c, surfID, wayland.SurfaceRequestAttach, uint32(0), int32(0), int32(0))
224	requestProtocol(t, c, surfID, wayland.SurfaceRequestCommit)
225	if err := c.Roundtrip(); err != nil {
226		t.Fatal(err)
227	}
228	ownerID, id := c.AllocateID(), c.AllocateID()
229	registerProtocol(t, c, ownerID)
230	s.display.Do(func() {
231		for _, surf := range s.surfaces {
232			if surf.wl.ID() != surfID {
233				continue
234			}
235			r, err := surf.wl.Client().CreateResource(extsessionlock.ExtSessionLockV1Interface, 1, ownerID, nil)
236			if err != nil {
237				t.Error(err)
238				return
239			}
240			if l, err := s.newLockSurface(extsessionlock.WrapExtSessionLockV1(r), id, surf, s.outputs[0], nil); l != nil || err != nil {
241				t.Error("accepted historical buffer")
242			}
243		}
244	})
245	expectProtocolError(t, c, ownerID, uint32(extsessionlock.ExtSessionLockV1ErrorAlreadyConstructed))
246}
247
248func lockSizedBuffer(t *testing.T, c *wlturbo.Display, w, h int) uint32 {
249	t.Helper()
250	shm := bindProtocol(t, c, "wl_shm")
251	registerProtocol(t, c, shm)
252	fd, err := unix.MemfdCreate("lock-geometry", 0)
253	if err != nil {
254		t.Fatal(err)
255	}
256	defer unix.Close(fd)
257	size := int32(w * h * 4)
258	if err := unix.Ftruncate(fd, int64(size)); err != nil {
259		t.Fatal(err)
260	}
261	pool, buffer := c.AllocateID(), c.AllocateID()
262	registerProtocol(t, c, pool)
263	registerProtocol(t, c, buffer)
264	if err := wireRequest(c, shm, uint16(wayland.ShmRequestCreatePool), []int{fd}, pool, size); err != nil {
265		t.Fatal(err)
266	}
267	requestProtocol(t, c, pool, wayland.ShmPoolRequestCreateBuffer, buffer, int32(0), int32(w), int32(h), int32(w*4), uint32(0))
268	return buffer
269}
270
271func TestLockSurfaceScaleTransform(t *testing.T) {
272	s, _, _, dir := lifecycleServer(t)
273	c := protocolClient(t, s, dir)
274	surfID, _, lockID, surf, l, configures := lockTestSurface(t, s, c)
275	<-configures
276	s.display.Do(func() { l.output.place.Width, l.output.place.Height = 3, 2; l.sendConfigure() })
277	if err := c.Roundtrip(); err != nil {
278		t.Fatal(err)
279	}
280	cfg := <-configures
281	requestProtocol(t, c, lockID, extsessionlock.ExtSessionLockSurfaceV1RequestAckConfigure, cfg.serial)
282	buffer := lockSizedBuffer(t, c, 4, 6)
283	requestProtocol(t, c, surfID, wayland.SurfaceRequestSetBufferScale, int32(2))
284	requestProtocol(t, c, surfID, wayland.SurfaceRequestSetBufferTransform, int32(1))
285	requestProtocol(t, c, surfID, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0))
286	requestProtocol(t, c, surfID, wayland.SurfaceRequestCommit)
287	if err := c.Roundtrip(); err != nil {
288		t.Fatal(err)
289	}
290	s.display.Do(func() {
291		if !l.mapped || surf.content.LogicalW != 3 || surf.content.LogicalH != 2 {
292			t.Errorf("scaled transformed content = %+v", surf.content)
293		}
294	})
295	requestProtocol(t, c, surfID, wayland.SurfaceRequestDestroy)
296	if err := c.Roundtrip(); err != nil {
297		t.Fatal(err)
298	}
299	s.display.Do(func() {
300		if !l.closed || len(s.lockSurfaces) != 0 {
301			t.Error("wl_surface destruction did not unmap")
302		}
303	})
304}
305
306func TestLockSurfaceDestroyedReplacementAndViewportRetainsGeometry(t *testing.T) {
307	s, _, _, dir := lifecycleServer(t)
308	c := protocolClient(t, s, dir)
309	surfID, _, lockID, surf, l, configures := lockTestSurface(t, s, c)
310	cfg := <-configures
311	vp := lockViewport(t, c, surfID, cfg.width, cfg.height)
312	requestProtocol(t, c, lockID, extsessionlock.ExtSessionLockSurfaceV1RequestAckConfigure, cfg.serial)
313	first := shmBuffer(t, c)
314	requestProtocol(t, c, surfID, wayland.SurfaceRequestAttach, first, int32(0), int32(0))
315	requestProtocol(t, c, surfID, wayland.SurfaceRequestCommit)
316	if err := c.Roundtrip(); err != nil {
317		t.Fatal(err)
318	}
319	s.display.Do(func() { l.output.place.Width++; l.sendConfigure() })
320	if err := c.Roundtrip(); err != nil {
321		t.Fatal(err)
322	}
323	newer := <-configures
324	requestProtocol(t, c, lockID, extsessionlock.ExtSessionLockSurfaceV1RequestAckConfigure, newer.serial)
325	requestProtocol(t, c, vp, viewporter.WpViewportRequestSetDestination, int32(newer.width), int32(newer.height))
326	replacement := shmBuffer(t, c)
327	requestProtocol(t, c, surfID, wayland.SurfaceRequestAttach, replacement, int32(0), int32(0))
328	if err := c.Roundtrip(); err != nil {
329		t.Fatal(err)
330	}
331	s.display.Do(func() { surf.next.at = time.Now().Add(time.Hour); surf.Commit(surf.wl) })
332	requestProtocol(t, c, replacement, wayland.BufferRequestDestroy)
333	requestProtocol(t, c, vp, viewporter.WpViewportRequestDestroy)
334	if err := c.Roundtrip(); err != nil {
335		t.Fatal(err)
336	}
337	s.display.Do(func() {
338		surf.queue[0].at = time.Time{}
339		surf.queue[0].applyGraph()
340		if !l.mapped || surf.current.ID() != first || surf.content.LogicalW != cfg.width || surf.committedViewport.destW != int32(cfg.width) {
341			t.Error("destroyed replacement changed retained geometry")
342		}
343	})
344}
345
346func TestLockSurfaceDestroyedCurrentBufferAllowsNoAttachCommit(t *testing.T) {
347	s, _, _, dir := lifecycleServer(t)
348	c := protocolClient(t, s, dir)
349	surfID, _, lockID, surf, l, configures := lockTestSurface(t, s, c)
350	cfg := <-configures
351	lockViewport(t, c, surfID, cfg.width, cfg.height)
352	requestProtocol(t, c, lockID, extsessionlock.ExtSessionLockSurfaceV1RequestAckConfigure, cfg.serial)
353	buffer := shmBuffer(t, c)
354	requestProtocol(t, c, surfID, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0))
355	requestProtocol(t, c, surfID, wayland.SurfaceRequestCommit)
356	if err := c.Roundtrip(); err != nil {
357		t.Fatal(err)
358	}
359	requestProtocol(t, c, buffer, wayland.BufferRequestDestroy)
360	callback := c.AllocateID()
361	registerProtocol(t, c, callback)
362	requestProtocol(t, c, surfID, wayland.SurfaceRequestFrame, callback)
363	requestProtocol(t, c, surfID, wayland.SurfaceRequestCommit)
364	if err := c.Roundtrip(); err != nil {
365		t.Fatal(err)
366	}
367	s.display.Do(func() {
368		if !l.mapped || !surf.has || surf.content.LogicalW != cfg.width {
369			t.Error("retained destroyed buffer unmapped")
370		}
371	})
372}
373
374func TestLockSurfaceDestroyBlocksLayerReuse(t *testing.T) {
375	s, _, _, dir := lifecycleServer(t)
376	c := protocolClient(t, s, dir)
377	surfID, _, lockID, _, _, _ := lockTestSurface(t, s, c)
378	requestProtocol(t, c, lockID, extsessionlock.ExtSessionLockSurfaceV1RequestDestroy)
379	if err := c.Roundtrip(); err != nil {
380		t.Fatal(err)
381	}
382	shell := bindVersion(t, c, "zwlr_layer_shell_v1", 4)
383	requestProtocol(t, c, shell, wlrlayershell.ZwlrLayerShellV1RequestGetLayerSurface, c.AllocateID(), surfID, uint32(0), uint32(2), "role-reuse")
384	expectProtocolError(t, c, shell, uint32(wlrlayershell.ZwlrLayerShellV1ErrorAlreadyConstructed))
385}
386
387func TestLockSurfaceDestroyRetiresFencedChildDependencies(t *testing.T) {
388	for _, destroy := range []string{"role", "surface", "role ready"} {
389		t.Run(destroy, func(t *testing.T) {
390			h := newSyncHarness(t)
391			h.releases = map[uint32]*syncReleaseProxy{}
392			c, s := h.c, h.s
393			rootID, _, lockID, root, _, configs := lockTestSurface(t, s, c)
394			cfg := <-configs
395			lockViewport(t, c, rootID, cfg.width, cfg.height)
396			requestProtocol(t, c, lockID, extsessionlock.ExtSessionLockSurfaceV1RequestAckConfigure, cfg.serial)
397			rootBuffer := shmBuffer(t, c)
398			requestProtocol(t, c, rootID, wayland.SurfaceRequestAttach, rootBuffer, int32(0), int32(0))
399			requestProtocol(t, c, rootID, wayland.SurfaceRequestCommit)
400			if err := c.Roundtrip(); err != nil {
401				t.Fatal(err)
402			}
403			comp := bindProtocol(t, c, "wl_compositor")
404			subcomp := bindProtocol(t, c, "wl_subcompositor")
405			mgr := bindProtocol(t, c, "wp_linux_drm_syncobj_manager_v1")
406			childID := c.AllocateID()
407			registerProtocol(t, c, childID)
408			requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, childID)
409			sub := c.AllocateID()
410			registerProtocol(t, c, sub)
411			requestProtocol(t, c, subcomp, wayland.SubcompositorRequestGetSubsurface, sub, childID, rootID)
412			syncID := c.AllocateID()
413			registerProtocol(t, c, syncID)
414			requestProtocol(t, c, mgr, linuxdrmsyncobj.WpLinuxDrmSyncobjManagerV1RequestGetSurface, syncID, childID)
415			h.surf, h.syncSurf = childID, syncID
416			buffer := h.dmabuf()
417			h.commit(buffer, 31, 32)
418			var child *surface
419			var dep, parent *update
420			var wait *syncWait
421			var tl *timeline
422			s.display.Do(func() {
423				for _, v := range s.surfaces {
424					if v.wl.ID() == childID {
425						child = v
426					}
427				}
428				root.next.at = time.Now().Add(time.Hour)
429				root.Commit(root.wl)
430				dep, parent = child.queue[0], root.queue[0]
431				dep.refs++ // Pin for inspection just like another graph consumer.
432				parent.refs++
433				wait, tl = dep.sync.wait, dep.sync.acquire.tl
434				if !dep.synced || !dep.bound || len(parent.deps) != 1 || tl.uses != 2 {
435					t.Error("child was not captured with its fence")
436				}
437			})
438			if destroy == "role ready" {
439				h.fire(31)
440				deadline := time.NewTimer(time.Second)
441				defer deadline.Stop()
442				ready := false
443				for !ready {
444					s.display.Do(func() { ready = child.syncReady(dep.sync) })
445					if ready {
446						break
447					}
448					select {
449					case <-deadline.C:
450						t.Fatal("acquire waiter did not become ready")
451					case <-time.After(time.Millisecond):
452					}
453				}
454			}
455			// A later child commit is not owned by the captured root graph.
456			requestProtocol(t, c, childID, wayland.SurfaceRequestCommit)
457			if err := c.Roundtrip(); err != nil {
458				t.Fatal(err)
459			}
460			if destroy != "surface" {
461				requestProtocol(t, c, lockID, extsessionlock.ExtSessionLockSurfaceV1RequestDestroy)
462			} else {
463				requestProtocol(t, c, rootID, wayland.SurfaceRequestDestroy)
464			}
465			if err := c.Roundtrip(); err != nil {
466				t.Fatal(err)
467			}
468			s.display.Do(func() {
469				remaining, refs := 1, 2 // later child commit retains its prev link
470				if destroy == "surface" {
471					remaining, refs = 0, 1 // parent destruction detaches the child
472				}
473				if len(root.queue) != 0 || len(child.queue) != remaining || !dep.retired || !parent.retired || dep.refs != refs || parent.refs != 1 || !wait.cancelled.Load() || tl.uses != 0 {
474					t.Errorf("dependency teardown: child=%d dep refs=%d parent refs=%d uses=%d cancelled=%v", len(child.queue), dep.refs, parent.refs, tl.uses, wait.cancelled.Load())
475				}
476				dep.applyGraph()
477				parent.applyGraph()
478				if child.current != nil || root.current != nil {
479					t.Error("retired graph replayed")
480				}
481				child.dropQueue()
482				if dep.refs != 1 {
483					t.Errorf("retired dependency refs after later commit retirement = %d", dep.refs)
484				}
485				dep.releaseRef()
486				parent.releaseRef()
487			})
488			count := 0
489			for _, point := range h.signalled() {
490				if point == 32 {
491					count++
492				}
493			}
494			want := 0
495			if destroy == "role ready" {
496				want = 1
497			}
498			if count != want {
499				t.Errorf("release signals = %d, want %d", count, want)
500			}
501			select {
502			case <-h.releases[buffer].released:
503				t.Error("explicit-sync buffer received wl_buffer.release")
504			default:
505			}
506		})
507	}
508}
509
510func TestLockSurfaceChildPrefixKeepsSharedBufferOwnership(t *testing.T) {
511	for _, preserved := range []string{"suffix", "pending", "implicit suffix"} {
512		t.Run(preserved, func(t *testing.T) {
513			var s *Server
514			var c *wlturbo.Display
515			var h *syncHarness
516			if preserved == "implicit suffix" {
517				h = newSyncHarness(t)
518				h.releases = map[uint32]*syncReleaseProxy{}
519				s, c = h.s, h.c
520			} else {
521				var dir string
522				s, _, _, dir = lifecycleServer(t)
523				c = protocolClient(t, s, dir)
524			}
525			rootID, _, lockID, root, _, configs := lockTestSurface(t, s, c)
526			cfg := <-configs
527			lockViewport(t, c, rootID, cfg.width, cfg.height)
528			requestProtocol(t, c, lockID, extsessionlock.ExtSessionLockSurfaceV1RequestAckConfigure, cfg.serial)
529			requestProtocol(t, c, rootID, wayland.SurfaceRequestAttach, shmBuffer(t, c), int32(0), int32(0))
530			requestProtocol(t, c, rootID, wayland.SurfaceRequestCommit)
531			comp := bindProtocol(t, c, "wl_compositor")
532			subcomp := bindProtocol(t, c, "wl_subcompositor")
533			childID := c.AllocateID()
534			registerProtocol(t, c, childID)
535			requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, childID)
536			sub := c.AllocateID()
537			registerProtocol(t, c, sub)
538			requestProtocol(t, c, subcomp, wayland.SubcompositorRequestGetSubsurface, sub, childID, rootID)
539			var buffer uint32
540			if h != nil {
541				buffer = h.dmabuf()
542			} else {
543				buffer = shmBuffer(t, c)
544			}
545			releases := &syncReleaseProxy{released: make(chan struct{}, 4)}
546			releases.SetID(buffer)
547			registerWireProxy(c, releases)
548			requestProtocol(t, c, childID, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0))
549			requestProtocol(t, c, childID, wayland.SurfaceRequestCommit)
550			if err := c.Roundtrip(); err != nil {
551				t.Fatal(err)
552			}
553			s.display.Do(func() { root.next.at = time.Now().Add(time.Hour); root.Commit(root.wl) })
554			requestProtocol(t, c, childID, wayland.SurfaceRequestAttach, buffer, int32(0), int32(0))
555			if preserved != "pending" {
556				requestProtocol(t, c, childID, wayland.SurfaceRequestCommit)
557			}
558			requestProtocol(t, c, lockID, extsessionlock.ExtSessionLockSurfaceV1RequestDestroy)
559			if err := c.Roundtrip(); err != nil {
560				t.Fatal(err)
561			}
562			select {
563			case <-releases.released:
564				t.Fatal("captured prefix released shared buffer early")
565			default:
566			}
567			// Convert the pending reference to an update, then retire the last
568			// reference through the same normal queue path.
569			if preserved == "pending" {
570				requestProtocol(t, c, childID, wayland.SurfaceRequestCommit)
571				if err := c.Roundtrip(); err != nil {
572					t.Fatal(err)
573				}
574			}
575			s.display.Do(func() {
576				child := s.surfaceOf(findTestSurface(s, childID))
577				if len(child.queue) != 1 {
578					t.Errorf("preserved queue length = %d", len(child.queue))
579				}
580				child.dropQueue()
581			})
582			if err := c.Roundtrip(); err != nil {
583				t.Fatal(err)
584			}
585			select {
586			case <-releases.released:
587			default:
588				t.Fatal("last queued reference did not release buffer")
589			}
590			select {
591			case <-releases.released:
592				t.Fatal("buffer released twice")
593			default:
594			}
595		})
596	}
597}
598
599func findTestSurface(s *Server, id uint32) *wayland.Surface {
600	for _, surf := range s.surfaces {
601		if surf.wl.ID() == id {
602			return surf.wl
603		}
604	}
605	return nil
606}

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 25

Tags 10

↑↓ moveenter openesc close