main 5004296

session_lock_test.go RawHistoryFolder

1//go:build linux
2
3package client_test
4
5import (
6	"errors"
7	"reflect"
8	"sync"
9	"testing"
10
11	"github.com/bnema/go-wayland-bindings/client/extsessionlock"
12	"github.com/bnema/go-wayland-bindings/client/wayland"
13	"github.com/bnema/wlturbo"
14)
15
16// These tests use ordinary generated proxies and the production transport on a
17// socketpair. Only the server wire peer is scripted; no request/dispatch hooks
18// are replaced, and no live compositor or session lock is used.
19func TestSessionLockLockedLifetime(t *testing.T) {
20	s := newWireEnv(t)
21	ctx := s.d.Context()
22	compositor := wayland.NewCompositor(ctx)
23	s.bind(1, wayland.CompositorInterface, 6, 6, compositor)
24	surface, err := compositor.CreateSurface()
25	noerr(t, err)
26	wantWords(t, "create_surface", s.req(compositor.ID(), 0), surface.ID())
27	out := wayland.NewOutput(ctx)
28	s.bind(2, wayland.OutputInterface, 4, 4, out)
29	mgr := extsessionlock.NewExtSessionLockManager(ctx)
30	s.bind(3, extsessionlock.ExtSessionLockManagerInterface, 1, 1, mgr)
31	lock, err := mgr.Lock()
32	noerr(t, err)
33	wantWords(t, "lock", s.req(mgr.ID(), 1), lock.ID())
34	if lock.Version() != 1 || lock.Context() != ctx {
35		t.Fatalf("lock version=%d context=%p", lock.Version(), lock.Context())
36	}
37	noerr(t, ctx.CheckProxy(lock))
38
39	// The manager destructor must not invalidate its lock children.
40	noerr(t, mgr.Destroy())
41	wantWords(t, "manager.destroy", s.req(mgr.ID(), 0))
42	if ctx.CheckProxy(mgr) == nil {
43		t.Fatal("destroyed manager remains registered")
44	}
45	s.event(1, 1, wordBytes(mgr.ID())...) // wl_display.delete_id
46	noerr(t, ctx.CheckProxy(lock))
47	ls, err := lock.GetLockSurface(surface, out)
48	noerr(t, err)
49	wantWords(t, "get_lock_surface", s.req(lock.ID(), 1), ls.ID(), surface.ID(), out.ID())
50	if ls.Version() != 1 || ls.Context() != ctx {
51		t.Fatalf("lock surface version=%d context=%p", ls.Version(), ls.Context())
52	}
53	noerr(t, ctx.CheckProxy(ls))
54
55	var events []string
56	lock.OnLocked(func() { events = append(events, "locked") })
57	lock.OnFinished(func() { events = append(events, "finished") })
58	var configures [][3]uint32
59	ls.OnConfigure(func(serial, width, height uint32) {
60		configures = append(configures, [3]uint32{serial, width, height})
61		noerr(t, ls.AckConfigure(serial))
62	})
63	s.event(ls.ID(), 0, wordBytes(0x89abcdef, 1920, 1080)...)
64	wantWords(t, "ack_configure", s.req(ls.ID(), 1), 0x89abcdef)
65	s.event(lock.ID(), 0)
66	s.event(lock.ID(), 1) // finished after locked still requires unlock_and_destroy
67	if !reflect.DeepEqual(events, []string{"locked", "finished"}) ||
68		!reflect.DeepEqual(configures, [][3]uint32{{0x89abcdef, 1920, 1080}}) {
69		t.Fatalf("events=%v configures=%v", events, configures)
70	}
71	// Neither event is a destructor; the application must choose the request.
72	noerr(t, ctx.CheckProxy(lock))
73	noerr(t, lock.UnlockAndDestroy())
74	wantWords(t, "unlock_and_destroy", s.req(lock.ID(), 2))
75	if ctx.CheckProxy(lock) == nil {
76		t.Fatal("unlocked lock remains registered")
77	}
78	s.event(1, 1, wordBytes(lock.ID())...)
79
80	// Lock destruction also leaves its children and core objects valid.
81	noerr(t, ctx.CheckProxy(ls))
82	noerr(t, ctx.CheckProxy(surface))
83	noerr(t, ctx.CheckProxy(out))
84	s.event(ls.ID(), 0, wordBytes(0xffffffff, 800, 600)...)
85	wantWords(t, "ack after parent destruction", s.req(ls.ID(), 1), 0xffffffff)
86	if len(configures) != 2 || configures[1] != [3]uint32{0xffffffff, 800, 600} {
87		t.Fatal(configures)
88	}
89	noerr(t, ls.Destroy())
90	wantWords(t, "surface.destroy", s.req(ls.ID(), 0))
91	if ctx.CheckProxy(ls) == nil {
92		t.Fatal("destroyed lock surface remains registered")
93	}
94	// Events in flight on a destroyed object are dropped until delete_id.
95	s.event(ls.ID(), 0, wordBytes(12, 640, 480)...)
96	if len(configures) != 2 {
97		t.Fatal("configure delivered to destroyed surface")
98	}
99	s.event(1, 1, wordBytes(ls.ID())...)
100	if err := ls.AckConfigure(12); err == nil {
101		t.Fatal("ack on destroyed surface succeeded")
102	}
103	if err := ls.Destroy(); err == nil {
104		t.Fatal("second surface destructor succeeded")
105	}
106	if err := lock.Destroy(); err == nil {
107		t.Fatal("second lock destructor succeeded")
108	}
109	if child, err := mgr.Lock(); err == nil || child != nil {
110		t.Fatal("destroyed manager created a child", child, err)
111	}
112	if child, err := lock.GetLockSurface(surface, out); err == nil || child != nil {
113		t.Fatal("destroyed lock created a child", child, err)
114	}
115	s.noRequest()
116	noerr(t, surface.Destroy())
117	wantWords(t, "wl_surface.destroy", s.req(surface.ID(), 0))
118	noerr(t, out.Release())
119	wantWords(t, "wl_output.release", s.req(out.ID(), 0))
120}
121
122func TestSessionLockDeniedDestroy(t *testing.T) {
123	s := newWireEnv(t)
124	mgr := extsessionlock.NewExtSessionLockManager(s.d.Context())
125	s.bind(1, extsessionlock.ExtSessionLockManagerInterface, 1, 1, mgr)
126	lock, err := mgr.Lock()
127	noerr(t, err)
128	wantWords(t, "lock", s.req(mgr.ID(), 1), lock.ID())
129	var locked, finished bool
130	lock.OnLocked(func() { locked = true })
131	lock.OnFinished(func() { finished = true })
132	s.event(lock.ID(), 1)
133	if locked || !finished {
134		t.Fatalf("locked=%t finished=%t", locked, finished)
135	}
136	noerr(t, s.d.Context().CheckProxy(lock))
137	noerr(t, lock.Destroy()) // never locked: destroy, not unlock_and_destroy
138	wantWords(t, "denied.destroy", s.req(lock.ID(), 0))
139	if err := lock.UnlockAndDestroy(); err == nil {
140		t.Fatal("second destructor succeeded")
141	}
142	s.noRequest()
143	// Destroying one child must not invalidate the manager or its next child.
144	noerr(t, s.d.Context().CheckProxy(mgr))
145	next, err := mgr.Lock()
146	noerr(t, err)
147	wantWords(t, "next lock", s.req(mgr.ID(), 1), next.ID())
148	noerr(t, next.Destroy())
149	wantWords(t, "next.destroy", s.req(next.ID(), 0))
150	noerr(t, mgr.Destroy())
151	wantWords(t, "manager.destroy", s.req(mgr.ID(), 0))
152}
153
154func TestSessionLockConcurrentDestructors(t *testing.T) {
155	s := newWireEnv(t)
156	mgr := extsessionlock.NewExtSessionLockManager(s.d.Context())
157	s.bind(1, extsessionlock.ExtSessionLockManagerInterface, 1, 1, mgr)
158	lock, err := mgr.Lock()
159	noerr(t, err)
160	wantWords(t, "lock", s.req(mgr.ID(), 1), lock.ID())
161	s.event(lock.ID(), 1)
162	var wg sync.WaitGroup
163	results := make(chan error, 8)
164	for range 8 {
165		wg.Add(1)
166		go func() { defer wg.Done(); results <- lock.Destroy() }()
167	}
168	wg.Wait()
169	close(results)
170	successes := 0
171	for err := range results {
172		if err == nil {
173			successes++
174		}
175	}
176	if successes != 1 {
177		t.Fatalf("successful destructors=%d, want 1", successes)
178	}
179	wantWords(t, "single destructor", s.req(lock.ID(), 0))
180	s.noRequest()
181}
182
183func TestSessionLockMalformedEvents(t *testing.T) {
184	for _, tc := range []struct {
185		name    string
186		surface bool
187		op      uint16
188		body    []byte
189	}{
190		{"locked trailing word", false, 0, wordBytes(1)},
191		{"finished trailing word", false, 1, wordBytes(1)},
192		{"configure missing height", true, 0, wordBytes(1, 800)},
193		{"configure trailing word", true, 0, wordBytes(1, 800, 600, 1)},
194	} {
195		t.Run(tc.name, func(t *testing.T) {
196			s := newWireEnv(t)
197			mgr := extsessionlock.NewExtSessionLockManager(s.d.Context())
198			s.bind(1, extsessionlock.ExtSessionLockManagerInterface, 1, 1, mgr)
199			lock, err := mgr.Lock()
200			noerr(t, err)
201			s.req(mgr.ID(), 1)
202			id := lock.ID()
203			called := false
204			lock.OnLocked(func() { called = true })
205			lock.OnFinished(func() { called = true })
206			if tc.surface {
207				ls, err := lock.GetLockSurface(s.surface(), s.output())
208				noerr(t, err)
209				s.req(lock.ID(), 1)
210				ls.OnConfigure(func(_, _, _ uint32) { called = true })
211				id = ls.ID()
212			}
213			send(t, s.p, payload(id, tc.op, tc.body...))
214			if err := s.d.Dispatch(); !errors.Is(err, wlturbo.ErrMalformedFrame) {
215				t.Fatalf("malformed event: %v", err)
216			}
217			if called {
218				t.Fatal("malformed event reached a handler")
219			}
220		})
221	}
222}

Keyboard shortcuts

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

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

Branches 1

Tags 1

↑↓ moveenter openesc close