main f2d166b

commit_lifecycle_test.go RawHistoryFolder

1package wayland
2
3import (
4	"reflect"
5	"testing"
6	"time"
7
8	"github.com/bnema/go-wayland-bindings/server/fifo"
9	"github.com/bnema/go-wayland-bindings/server/presentationtime"
10	"github.com/bnema/go-wayland-bindings/server/viewporter"
11	"github.com/bnema/go-wayland-bindings/server/wayland"
12	"github.com/bnema/go-wayland-bindings/server/wlrlayershell"
13	"github.com/bnema/go-wayland-bindings/server/xdgshell"
14	"github.com/bnema/neferwl/internal/ports"
15	"github.com/bnema/purego-libwayland/server"
16)
17
18// Applying an older captured commit must never transiently own newer requests.
19// The next commit captures those requests without losing its one-shot state.
20func TestCapturedCommitKeepsLaterRequests(t *testing.T) {
21	srv := &Server{fifoSurfaces: map[*surface]struct{}{}, constraints: map[*surface]*constraint{}}
22	surf := &surface{server: srv, xdg: &xdgSurface{}}
23	cons := &constraint{surface: surf}
24	srv.constraints[surf] = cons
25	firstRegion := &ports.Rect{W: 2}
26	cons.pending = firstRegion
27	surf.next.scale, surf.next.async, surf.next.kind = 2, true, 1
28	surf.xdg.pendingGeometry = ports.Rect{W: 10}
29	surf.next.inputSet, surf.next.inputAll = true, true
30	surf.next.damage = []ports.Rect{{W: 2}}
31	surf.next.barrier = true
32	surf.queueUpdate()
33
34	laterRegion := &ports.Rect{W: 3}
35	cons.pending = laterRegion
36	surf.next.scale, surf.next.async, surf.next.kind = 3, false, 2
37	surf.sub.pendingLayout = []childLayout{{x: 7}}
38	surf.xdg.pendingGeometry = ports.Rect{W: 20}
39	surf.next.inputSet, surf.next.inputAll = true, false
40	surf.next.inputRects = []ports.Rect{{W: 3}}
41	surf.next.damage = []ports.Rect{{W: 3}}
42	surf.next.barrier = true
43	laterTime := time.Now().Add(time.Second)
44	surf.next.at = laterTime
45
46	surf.queue[0].applyGraph()
47	if surf.bufferScale != 2 || !surf.async || surf.contentKind != 1 || len(surf.sub.layout) != 0 || surf.xdg.geometry.W != 10 || cons.region.W != 2 || !surf.inputAll || !surf.barrier {
48		t.Fatalf("captured state changed: scale=%d async=%v kind=%d geometry=%v region=%v input=%v barrier=%v", surf.bufferScale, surf.async, surf.contentKind, surf.xdg.geometry, cons.region, surf.inputAll, surf.barrier)
49	}
50	if surf.next.scale != 3 || surf.next.async || surf.next.kind != 2 || len(surf.sub.pendingLayout) != 1 || surf.xdg.pendingGeometry.W != 20 || cons.pending != laterRegion || !surf.next.inputSet || surf.next.inputAll || len(surf.next.inputRects) != 1 || len(surf.next.damage) != 1 || !surf.next.barrier || !surf.next.at.Equal(laterTime) {
51		t.Fatal("application consumed a later request")
52	}
53	surf.queueUpdate()
54	second := surf.queue[0]
55	if second.scale != 3 || second.async || second.kind != 2 || len(second.layout) != 1 || second.geometry.W != 20 || second.region != laterRegion || !second.inputSet || second.inputAll || len(second.inputRects) != 1 || len(second.damage) != 1 || !second.barrier || !second.at.Equal(laterTime) {
56		t.Fatal("subsequent commit did not capture later requests")
57	}
58	surf.dropQueue()
59}
60
61// The viewport resource is live, but its requests after capture must not be
62// installed by an older queued commit.
63func TestCapturedViewportKeepsLaterRequests(t *testing.T) {
64	s, _, _, _, dir := contentServer(t)
65	c := protocolClient(t, s, dir)
66	comp := bindProtocol(t, c, "wl_compositor")
67	vpm := bindProtocol(t, c, "wp_viewporter")
68	surfID := c.AllocateID()
69	requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surfID)
70	registerProtocol(t, c, surfID)
71	vpID := c.AllocateID()
72	requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, vpID, surfID)
73	registerProtocol(t, c, vpID)
74	if err := c.Roundtrip(); err != nil {
75		t.Fatal(err)
76	}
77	s.display.Do(func() {
78		var surf *surface
79		for _, candidate := range s.surfaces {
80			if candidate.wl != nil && candidate.wl.ID() == surfID {
81				surf = candidate
82			}
83		}
84		if surf == nil || surf.viewport == nil {
85			t.Error("missing viewport")
86			return
87		}
88		v := surf.viewport
89		v.SetSource(v.resource, 0, 0, server.Fixed(256), server.Fixed(256))
90		v.SetDestination(v.resource, 1, 1)
91		surf.queueUpdate()
92		v.SetSource(v.resource, 0, 0, server.Fixed(512), server.Fixed(512))
93		v.SetDestination(v.resource, 2, 2)
94		surf.queue[0].applyGraph()
95		if surf.committedViewport.destW != 1 || surf.committedViewport.src[2] != 256 || v.pendingW != 2 || v.pendingSrc[2] != 512 {
96			t.Errorf("captured=%+v pending=%+v", surf.committedViewport, v)
97		}
98		surf.queueUpdate()
99		surf.queue[0].applyGraph()
100		if surf.committedViewport.destW != 2 || surf.committedViewport.src[2] != 512 {
101			t.Errorf("next commit: %+v", surf.committedViewport)
102		}
103	})
104}
105
106// Real protocol requests captured behind a barrier keep the first buffer;
107// an attach made after capture is owned by the next commit.
108func TestQueuedAttachKeepsLaterBuffer(t *testing.T) {
109	s, _, _, _, dir := contentServer(t)
110	c := protocolClient(t, s, dir)
111	comp := bindProtocol(t, c, "wl_compositor")
112	fm := bindProtocol(t, c, "wp_fifo_manager_v1")
113	id := c.AllocateID()
114	requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, id)
115	registerProtocol(t, c, id)
116	f := c.AllocateID()
117	requestProtocol(t, c, fm, fifo.WpFifoManagerV1RequestGetFifo, f, id)
118	registerProtocol(t, c, f)
119	first, second := shmBuffer(t, c), shmBuffer(t, c)
120	requestProtocol(t, c, f, fifo.WpFifoV1RequestSetBarrier)
121	requestProtocol(t, c, id, wayland.SurfaceRequestCommit)
122	requestProtocol(t, c, f, fifo.WpFifoV1RequestWaitBarrier)
123	requestProtocol(t, c, id, wayland.SurfaceRequestAttach, first, int32(0), int32(0))
124	requestProtocol(t, c, id, wayland.SurfaceRequestCommit)
125	requestProtocol(t, c, id, wayland.SurfaceRequestAttach, second, int32(0), int32(0))
126	if err := c.Roundtrip(); err != nil {
127		t.Fatal(err)
128	}
129	s.display.Do(func() {
130		var surf *surface
131		for _, candidate := range s.surfaces {
132			if candidate.wl != nil && candidate.wl.ID() == id {
133				surf = candidate
134			}
135		}
136		if surf == nil || len(surf.queue) != 1 || surf.queue[0].buffer.ID() != first || surf.next.buffer.ID() != second {
137			t.Error("queued attach lost its captured or later buffer")
138			return
139		}
140		surf.barrier = false
141		s.tickFifo(time.Now(), nil)
142		if surf.current == nil || surf.current.ID() != first || surf.next.buffer == nil || surf.next.buffer.ID() != second {
143			t.Error("applying captured attach consumed later request")
144		}
145	})
146	requestProtocol(t, c, id, wayland.SurfaceRequestCommit)
147	if err := c.Roundtrip(); err != nil {
148		t.Fatal(err)
149	}
150	s.display.Do(func() {
151		for _, surf := range s.surfaces {
152			if surf.wl != nil && surf.wl.ID() == id && (surf.current == nil || surf.current.ID() != second) {
153				t.Error("later attach not applied by next commit")
154			}
155		}
156	})
157}
158
159// Layer requests are sticky, but must not be read from live pending state
160// when an older queued update is finally applied.
161func TestCapturedLayerKeepsLaterRequests(t *testing.T) {
162	s, _, _, _, dir := contentServer(t)
163	c := protocolClient(t, s, dir)
164	comp := bindProtocol(t, c, "wl_compositor")
165	shell := bindProtocol(t, c, "zwlr_layer_shell_v1")
166	id := c.AllocateID()
167	requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, id)
168	registerProtocol(t, c, id)
169	layerID := c.AllocateID()
170	requestProtocol(t, c, shell, wlrlayershell.ZwlrLayerShellV1RequestGetLayerSurface, layerID, id, uint32(0), uint32(ports.LayerTop), "captured")
171	registerProtocol(t, c, layerID)
172	if err := c.Roundtrip(); err != nil {
173		t.Fatal(err)
174	}
175	s.display.Do(func() {
176		var surf *surface
177		for _, candidate := range s.surfaces {
178			if candidate.wl != nil && candidate.wl.ID() == id {
179				surf = candidate
180			}
181		}
182		if surf == nil || surf.layer == nil {
183			t.Error("missing layer")
184			return
185		}
186		l := surf.layer
187		l.SetSize(l.resource, 1, 1)
188		l.SetAnchor(l.resource, 1)
189		l.SetMargin(l.resource, 1, 2, 3, 4)
190		l.SetExclusiveZone(l.resource, 5)
191		surf.queueUpdate()
192		l.SetSize(l.resource, 2, 2)
193		l.SetAnchor(l.resource, 2)
194		l.SetMargin(l.resource, 5, 6, 7, 8)
195		l.SetExclusiveZone(l.resource, 9)
196		surf.queue[0].applyGraph()
197		if l.current.width != 1 || l.current.anchor != 1 || l.current.margin != [4]int32{1, 2, 3, 4} || l.current.zone != 5 || l.pending.width != 2 || l.pending.anchor != 2 || l.pending.margin != [4]int32{5, 6, 7, 8} || l.pending.zone != 9 {
198			t.Errorf("captured=%+v pending=%+v", l.current, l.pending)
199		}
200		surf.queueUpdate()
201		surf.queue[0].applyGraph()
202		if l.current.width != 2 || l.current.anchor != 2 || l.current.zone != 9 {
203			t.Errorf("next commit: %+v", l.current)
204		}
205	})
206}
207
208// Role and surface destruction drop their queues. Viewport and buffer
209// destruction leave the queued update to apply safely at the next tick.
210// Roundtrip orders destruction before examining the display owner.
211func TestQueuedDestructionDropsUpdates(t *testing.T) {
212	for _, kind := range []string{"viewport", "toplevel", "layer", "surface", "buffer"} {
213		t.Run(kind, func(t *testing.T) {
214			s, events, _, _, dir := contentServer(t)
215			c := protocolClient(t, s, dir)
216			var surfID, objectID, bufferID uint32
217			if kind == "toplevel" {
218				_, surfID, _ = surfaceMapper(t, c, events)()
219				s.display.Do(func() {
220					for _, surf := range s.surfaces {
221						if surf.wl != nil && surf.wl.ID() == surfID {
222							objectID = surf.xdg.window.toplevel.ID()
223						}
224					}
225				})
226			} else {
227				comp := bindProtocol(t, c, "wl_compositor")
228				surfID = c.AllocateID()
229				requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surfID)
230				registerProtocol(t, c, surfID)
231				if kind == "viewport" {
232					vpm := bindProtocol(t, c, "wp_viewporter")
233					objectID = c.AllocateID()
234					requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, objectID, surfID)
235					registerProtocol(t, c, objectID)
236				} else if kind == "layer" {
237					shell := bindProtocol(t, c, "zwlr_layer_shell_v1")
238					objectID = c.AllocateID()
239					requestProtocol(t, c, shell, wlrlayershell.ZwlrLayerShellV1RequestGetLayerSurface, objectID, surfID, uint32(0), uint32(ports.LayerTop), "drop")
240					registerProtocol(t, c, objectID)
241					requestProtocol(t, c, objectID, wlrlayershell.ZwlrLayerSurfaceV1RequestSetSize, uint32(1), uint32(1))
242				} else if kind == "buffer" {
243					bufferID = shmBuffer(t, c)
244				}
245			}
246			if err := c.Roundtrip(); err != nil {
247				t.Fatal(err)
248			}
249			s.display.Do(func() {
250				for _, surf := range s.surfaces {
251					if surf.wl != nil && surf.wl.ID() == surfID {
252						surf.next.scale = 3
253						surf.next.damage = []ports.Rect{{W: 3}}
254						if kind == "viewport" {
255							surf.viewport.SetDestination(surf.viewport.resource, 2, 2)
256						}
257						surf.next.at = time.Now().Add(time.Second)
258						if kind == "buffer" {
259							for resource := range s.buffers {
260								if resource.ID() == bufferID {
261									surf.next.buffer = wayland.WrapBuffer(resource)
262									surf.next.attached = true
263								}
264							}
265						}
266						surf.queueUpdate()
267					}
268				}
269			})
270			switch kind {
271			case "viewport":
272				requestProtocol(t, c, objectID, viewporter.WpViewportRequestDestroy)
273			case "toplevel":
274				requestProtocol(t, c, objectID, xdgshell.ToplevelRequestDestroy)
275			case "layer":
276				requestProtocol(t, c, objectID, wlrlayershell.ZwlrLayerSurfaceV1RequestDestroy)
277			case "surface":
278				requestProtocol(t, c, surfID, wayland.SurfaceRequestDestroy)
279			case "buffer":
280				requestProtocol(t, c, bufferID, wayland.BufferRequestDestroy)
281			}
282			if err := c.Roundtrip(); err != nil {
283				t.Fatal(err)
284			}
285			s.display.Do(func() {
286				for _, surf := range s.surfaces {
287					if surf.wl != nil && surf.wl.ID() == surfID {
288						if kind == "viewport" || kind == "buffer" {
289							if kind == "buffer" && (len(surf.queue) != 1 || surf.queue[0].buffer == nil || surf.queue[0].buffer.Resource.Alive()) {
290								t.Error("expected destroyed buffer in queued update")
291							}
292							if len(surf.queue) != 1 {
293								t.Errorf("%s should retain queued update until tick", kind)
294							}
295							s.tickFifo(time.Now().Add(2*time.Second), nil)
296						}
297						if len(surf.queue) != 0 {
298							t.Errorf("%s retained queued update", kind)
299						}
300					}
301				}
302				fresh := &surface{server: s}
303				fresh.queueUpdate()
304				u := fresh.queue[0]
305				if u.scale != 0 || u.buffer != nil || u.vp != nil || u.layer != nil || len(u.damage) != 0 || u.owner != fresh {
306					t.Errorf("%s recycled stale update: %+v", kind, u)
307				}
308				fresh.queue[0].applyGraph()
309			})
310		})
311	}
312}
313
314// A dropped update must be cleared before a different surface reuses it.
315func TestDroppedUpdateRecycledWithoutState(t *testing.T) {
316	srv := &Server{fifoSurfaces: map[*surface]struct{}{}}
317	old := &surface{server: srv, next: pendingCommit{scale: 2, barrier: true, damage: []ports.Rect{{W: 2}}}}
318	old.queueUpdate()
319	old.destroyed = true
320	old.dropQueue()
321	fresh := &surface{server: srv}
322	fresh.queueUpdate()
323	if u := fresh.queue[0]; u.scale != 0 || u.barrier || len(u.damage) != 0 || u.owner != fresh || u.buffer != nil || u.vp != nil || u.layer != nil || u.xdg != nil {
324		t.Fatalf("recycled update retained old state: %+v", u)
325	}
326	fresh.queue[0].applyGraph()
327}
328
329// BenchmarkEmptySurfaceQueueApply measures an empty, roleless surface's
330// in-process queue/apply loop. It excludes protocol dispatch and real buffers.
331func BenchmarkEmptySurfaceQueueApply(b *testing.B) {
332	srv := &Server{fifoSurfaces: map[*surface]struct{}{}}
333	surf := &surface{server: srv}
334	surf.queueUpdate()
335	surf.queue[0].applyGraph()
336	b.ReportAllocs()
337	b.ResetTimer()
338	for range b.N {
339		surf.queueUpdate()
340		surf.queue[0].applyGraph()
341	}
342}
343
344// TestCapturedViewportApplyAllocations exercises the real viewport resource
345// on the display owner: a viewport-only commit has no heap work.
346func TestCapturedViewportApplyAllocations(t *testing.T) {
347	s, _, _, _, dir := contentServer(t)
348	c := protocolClient(t, s, dir)
349	comp := bindProtocol(t, c, "wl_compositor")
350	vpm := bindProtocol(t, c, "wp_viewporter")
351	surfID := c.AllocateID()
352	requestProtocol(t, c, comp, wayland.CompositorRequestCreateSurface, surfID)
353	registerProtocol(t, c, surfID)
354	vpID := c.AllocateID()
355	requestProtocol(t, c, vpm, viewporter.WpViewporterRequestGetViewport, vpID, surfID)
356	registerProtocol(t, c, vpID)
357	if err := c.Roundtrip(); err != nil {
358		t.Fatal(err)
359	}
360	s.display.Do(func() {
361		var surf *surface
362		for _, candidate := range s.surfaces {
363			if candidate.wl != nil && candidate.wl.ID() == surfID {
364				surf = candidate
365				break
366			}
367		}
368		if surf == nil || surf.viewport == nil || surf.viewport.resource == nil {
369			t.Error("missing viewport")
370			return
371		}
372		apply := func() {
373			surf.queueUpdate()
374			surf.queue[0].applyGraph()
375		}
376		apply()
377		if allocs := testing.AllocsPerRun(100, apply); allocs != 0 {
378			t.Errorf("viewport queue/apply: %.1f allocs/commit; want 0", allocs)
379		}
380	})
381}
382
383func TestCapturedCommitApplyAllocations(t *testing.T) {
384	srv := &Server{fifoSurfaces: map[*surface]struct{}{}}
385	surf := &surface{server: srv}
386	apply := func() {
387		surf.queueUpdate()
388		surf.queue[0].applyGraph()
389	}
390	apply() // warm pool and queue backing storage
391	if allocs := testing.AllocsPerRun(100, apply); allocs != 0 {
392		t.Errorf("steady-state queue/apply: %.1f allocs/commit; want 0", allocs)
393	}
394}
395
396// Capture takes every requested field; only sticky hints carry over.
397func TestTakePendingKeepsStickyState(t *testing.T) {
398	srv := &Server{constraints: map[*surface]*constraint{}}
399	surf := &surface{server: srv}
400	all := pendingCommit{
401		buffer: &wayland.Buffer{}, attached: true, scale: 2, transform: 1,
402		opaque: []ports.Rect{{W: 1}}, opaqueSet: true, inputAll: true, inputSet: true, inputRects: []ports.Rect{{W: 1}},
403		callbacks: []*wayland.Callback{{}}, async: true, color: SurfaceColor{Set: true}, representation: surfaceRepresentation{alpha: 1}, kind: 3,
404		barrier: true, wait: true, at: time.Unix(1, 0), damage: []ports.Rect{{W: 1}}, bufDamage: []ports.Rect{{W: 1}},
405		feedback: []*presentationtime.WpPresentationFeedback{{}}, sync: &commitSync{},
406	}
407	surf.next = all
408	u := surf.takePending()
409	if !reflect.DeepEqual(u.pendingCommit, all) {
410		t.Fatalf("captured = %+v", u.pendingCommit)
411	}
412	want := pendingCommit{scale: 2, transform: 1, async: true, color: SurfaceColor{Set: true}, representation: surfaceRepresentation{alpha: 1}, kind: 3}
413	if !reflect.DeepEqual(surf.next, want) {
414		t.Fatalf("left pending = %+v, want %+v", surf.next, want)
415	}
416}

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