v0.2.0 e178e0b

conn_test.go RawHistoryFolder

1package neferclient_test
2
3import (
4	"context"
5	"testing"
6	"time"
7
8	"github.com/bnema/go-wayland-bindings/client/wayland"
9	"github.com/bnema/neferclient"
10	"github.com/stretchr/testify/assert"
11	"github.com/stretchr/testify/mock"
12	"github.com/stretchr/testify/require"
13	"golang.org/x/sys/unix"
14
15	neferclientmocks "github.com/bnema/neferclient/mocks"
16)
17
18// dispatchUntil dispatches whenever Wake fires until done reports true.
19func dispatchUntil(t *testing.T, c *neferclient.Conn, h neferclient.Handler, done func() bool) {
20	t.Helper()
21	timeout := time.After(10 * time.Second)
22	for {
23		require.NoError(t, c.Dispatch(h))
24		if done() {
25			return
26		}
27		select {
28		case <-c.Wake():
29		case <-timeout:
30			t.Fatal("timed out waiting for events")
31		}
32	}
33}
34
35func TestSocketPath(t *testing.T) {
36	t.Setenv("XDG_RUNTIME_DIR", "/run/user/1")
37	t.Setenv("WAYLAND_DISPLAY", "")
38	p, err := neferclient.SocketPath("")
39	require.NoError(t, err)
40	assert.Equal(t, "/run/user/1/wayland-0", p)
41	t.Setenv("WAYLAND_DISPLAY", "wayland-7")
42	p, _ = neferclient.SocketPath("")
43	assert.Equal(t, "/run/user/1/wayland-7", p)
44	p, _ = neferclient.SocketPath("x")
45	assert.Equal(t, "/run/user/1/x", p)
46	p, _ = neferclient.SocketPath("/tmp/sock")
47	assert.Equal(t, "/tmp/sock", p)
48	t.Setenv("XDG_RUNTIME_DIR", "")
49	_, err = neferclient.SocketPath("rel")
50	assert.Error(t, err)
51}
52
53func TestConnectInitialOutputs(t *testing.T) {
54	c, srv := connectWire(t,
55		[]wireGlobal{{1, "wl_seat", 9}, {7, outputIface, 4}, {9, outputIface, 2}},
56		map[uint32]outputSpec{7: {"DP-1", 2, 1920, 1080}, 9: {"ignored-before-v4", 1, 800, 600}})
57	assert.Equal(t, []neferclient.Output{
58		{Global: 7, Name: "DP-1", Scale: 2, Width: 1920, Height: 1080},
59		{Global: 9, Name: "", Scale: 1, Width: 800, Height: 600},
60	}, c.Outputs())
61	assert.Equal(t, uint32(4), srv.waitBound(7).version, "bound at min(announced, 4)")
62	assert.Equal(t, uint32(2), srv.waitBound(9).version)
63
64	// Setup events are applied silently: nothing is left for the Handler. The
65	// roundtrip moves everything handled so far into the ring, so a leaked
66	// setup event would reach the strict mock here.
67	h := neferclientmocks.NewMockHandler(t)
68	require.NoError(t, c.Roundtrip())
69	require.NoError(t, c.Dispatch(h))
70}
71
72func TestConnectContextCancelled(t *testing.T) {
73	client, _ := socketPair(t) // peer never answers
74	ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
75	defer cancel()
76	c, err := neferclient.ConnectConn(ctx, client)
77	require.Error(t, err)
78	assert.Nil(t, c)
79	assert.ErrorIs(t, err, context.DeadlineExceeded)
80
81	cancelled, stop := context.WithCancel(context.Background())
82	stop()
83	_, err = neferclient.Connect(cancelled, "")
84	assert.ErrorIs(t, err, context.Canceled)
85	//lint:ignore SA1012 a nil context is the case under test
86	_, err = neferclient.Connect(nil, "")
87	assert.Error(t, err)
88}
89
90func TestHotplug(t *testing.T) {
91	c, srv := connectWire(t, []wireGlobal{{7, outputIface, 4}},
92		map[uint32]outputSpec{7: {"DP-1", 1, 1920, 1080}, 11: {"HDMI-A-1", 2, 3840, 2160}})
93	h := neferclientmocks.NewMockHandler(t)
94	var added []neferclient.Output
95	h.EXPECT().OutputAdded(mock.Anything).Run(func(out *neferclient.Output) { added = append(added, *out) }).Once()
96	srv.announce(wireGlobal{11, outputIface, 4})
97	dispatchUntil(t, c, h, func() bool { return len(added) == 1 })
98	assert.Equal(t, neferclient.Output{Global: 11, Name: "HDMI-A-1", Scale: 2, Width: 3840, Height: 2160}, added[0])
99	require.Len(t, c.Outputs(), 2)
100
101	h.EXPECT().OutputRemoved(uint32(7)).Once()
102	srv.remove(7)
103	dispatchUntil(t, c, h, func() bool { return len(c.Outputs()) == 1 })
104	assert.Equal(t, uint32(11), c.Outputs()[0].Global)
105}
106
107func TestOutputPropertyChangeIsSilent(t *testing.T) {
108	c, srv := connectWire(t, []wireGlobal{{7, outputIface, 4}}, map[uint32]outputSpec{7: {"DP-1", 1, 100, 100}})
109	obj := srv.waitBound(7)
110	srv.write(frame(obj.id, 3, appendU32(nil, 3))) // scale 3
111	srv.write(frame(obj.id, 2, nil))               // done
112	h := neferclientmocks.NewMockHandler(t)        // no expectations: any call fails
113	dispatchUntil(t, c, h, func() bool { return c.Outputs()[0].Scale == 3 })
114}
115
116func TestRingBackpressureKeepsOrder(t *testing.T) {
117	const total = 3 * neferclient.RingSize
118	c, srv := connectWire(t, []wireGlobal{{7, outputIface, 4}}, map[uint32]outputSpec{7: {"DP-1", 1, 100, 100}})
119	obj := srv.waitBound(7)
120	var buf []byte
121	for i := 1; i <= total; i++ {
122		buf = append(buf, frame(obj.id, 3, appendU32(nil, uint32(i)))...)
123		buf = append(buf, frame(obj.id, 2, nil)...)
124	}
125	go srv.write(buf)
126	waitFor(t, func() bool { return c.QueueLen() == neferclient.RingSize }, "full ring")
127
128	// A pause (Roundtrip) must not deadlock against a producer blocked on the
129	// full ring, and must not reorder what it held back: the visible scale
130	// only ever grows, ending at total.
131	require.NoError(t, c.Roundtrip())
132	prev := int32(0)
133	dispatchUntil(t, c, nil, func() bool {
134		got := c.Outputs()[0].Scale
135		require.GreaterOrEqual(t, got, prev, "scale went backwards: events were reordered")
136		prev = got
137		return got == total
138	})
139}
140
141func TestRoundtripQueuesEventsForNextDispatch(t *testing.T) {
142	c, srv := connectWire(t, []wireGlobal{{7, outputIface, 4}}, map[uint32]outputSpec{7: {"DP-1", 1, 100, 100}})
143	obj := srv.waitBound(7)
144	srv.write(frame(obj.id, 3, appendU32(nil, 4))) // scale 4
145	srv.write(frame(obj.id, 2, nil))               // done
146	require.NoError(t, c.Roundtrip())
147	// No Wake wait: the events handled during the roundtrip are already queued.
148	require.NoError(t, c.Dispatch(nil))
149	assert.Equal(t, int32(4), c.Outputs()[0].Scale)
150}
151
152func TestBindQueuesEventsForNextDispatch(t *testing.T) {
153	c, srv := connectWire(t, []wireGlobal{{7, outputIface, 4}, {3, wayland.CompositorInterface, 6}},
154		map[uint32]outputSpec{7: {"DP-1", 1, 100, 100}})
155	obj := srv.waitBound(7)
156	srv.write(frame(obj.id, 3, appendU32(nil, 2)))
157	srv.write(frame(obj.id, 2, nil))
158	// The bind pauses the reader; wait until the reader has the events so they
159	// are handled during the pause or before it, never after Bind returns.
160	waitFor(t, func() bool { return c.QueueLen() == 2 }, "events queued")
161	_, _, err := c.Bind[*wayland.Compositor](wayland.CompositorInterface, 4, wayland.NewCompositor)
162	require.NoError(t, err)
163	require.NoError(t, c.Dispatch(nil))
164	assert.Equal(t, int32(2), c.Outputs()[0].Scale)
165}
166
167func waitFor(t *testing.T, cond func() bool, what string) {
168	t.Helper()
169	deadline := time.Now().Add(10 * time.Second)
170	for !cond() {
171		if time.Now().After(deadline) {
172			t.Fatalf("timed out waiting for %s", what)
173		}
174		time.Sleep(time.Millisecond)
175	}
176}
177
178func TestBind(t *testing.T) {
179	c, srv := connectWire(t, []wireGlobal{{3, wayland.CompositorInterface, 6}}, nil)
180	comp, v, err := c.Bind[*wayland.Compositor](wayland.CompositorInterface, 4, wayland.NewCompositor)
181	require.NoError(t, err)
182	assert.NotNil(t, comp)
183	assert.Equal(t, uint32(4), v)
184	assert.Equal(t, uint32(4), srv.waitBound(3).version)
185
186	_, _, err = c.Bind[*wayland.Shm](wayland.ShmInterface, 1, wayland.NewShm)
187	assert.ErrorIs(t, err, neferclient.ErrGlobalNotFound)
188}
189
190func TestWatchFD(t *testing.T) {
191	c, _ := connectWire(t, nil, nil)
192	efd, err := unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK)
193	require.NoError(t, err)
194	defer unix.Close(efd)
195	require.NoError(t, c.WatchFD(efd, 42))
196	assert.Error(t, c.WatchFD(efd, 43), "double watch")
197
198	h := neferclientmocks.NewMockHandler(t)
199	got := 0
200	h.EXPECT().FDReady(uint64(42)).Run(func(uint64) {
201		got++
202		var b [8]byte
203		_, _ = unix.Read(efd, b[:])
204	}).Times(2)
205	signal := func() {
206		one := [8]byte{1}
207		_, err := unix.Write(efd, one[:])
208		require.NoError(t, err)
209	}
210	signal()
211	dispatchUntil(t, c, h, func() bool { return got == 1 })
212	signal() // re-armed after the first FDReady returned
213	dispatchUntil(t, c, h, func() bool { return got == 2 })
214
215	require.NoError(t, c.UnwatchFD(efd))
216	assert.Error(t, c.UnwatchFD(efd))
217}
218
219func TestUnwatchDropsQueuedNotification(t *testing.T) {
220	c, _ := connectWire(t, nil, nil)
221	efd, err := unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK)
222	require.NoError(t, err)
223	defer unix.Close(efd)
224	require.NoError(t, c.WatchFD(efd, 9))
225	one := [8]byte{1}
226	_, err = unix.Write(efd, one[:])
227	require.NoError(t, err)
228	waitFor(t, func() bool { return c.QueueLen() > 0 }, "queued fd notification")
229	require.NoError(t, c.UnwatchFD(efd))
230	h := neferclientmocks.NewMockHandler(t) // no FDReady expectation: a call fails
231	require.NoError(t, c.Dispatch(h))
232}
233
234func TestCloseInsideFDReady(t *testing.T) {
235	c, _ := connectWire(t, nil, nil)
236	efd, err := unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK)
237	require.NoError(t, err)
238	defer unix.Close(efd)
239	require.NoError(t, c.WatchFD(efd, 1))
240	h := neferclientmocks.NewMockHandler(t) // Error must not be called after Close
241	h.EXPECT().FDReady(uint64(1)).Run(func(uint64) { require.NoError(t, c.Close()) }).Once()
242	one := [8]byte{1}
243	_, _ = unix.Write(efd, one[:])
244	waitFor(t, func() bool { return c.QueueLen() > 0 }, "queued fd notification")
245	assert.ErrorIs(t, c.Dispatch(h), neferclient.ErrClosed)
246}
247
248func TestOutputVersionOneIgnored(t *testing.T) {
249	c, _ := connectWire(t, []wireGlobal{{5, outputIface, 1}, {7, outputIface, 2}},
250		map[uint32]outputSpec{5: {"old", 1, 10, 10}, 7: {"DP-1", 1, 100, 100}})
251	require.Len(t, c.Outputs(), 1)
252	assert.Equal(t, uint32(7), c.Outputs()[0].Global)
253}
254
255func TestUnwatchInsideHandler(t *testing.T) {
256	c, _ := connectWire(t, nil, nil)
257	efd, err := unix.Eventfd(0, unix.EFD_CLOEXEC|unix.EFD_NONBLOCK)
258	require.NoError(t, err)
259	defer unix.Close(efd)
260	require.NoError(t, c.WatchFD(efd, 1))
261	h := neferclientmocks.NewMockHandler(t)
262	called := false
263	h.EXPECT().FDReady(uint64(1)).Run(func(uint64) {
264		called = true
265		require.NoError(t, c.UnwatchFD(efd))
266	}).Once()
267	one := [8]byte{1}
268	_, _ = unix.Write(efd, one[:])
269	dispatchUntil(t, c, h, func() bool { return called })
270}
271
272func TestTransportErrorSurfacesAfterQueuedEvents(t *testing.T) {
273	c, srv := connectWire(t, []wireGlobal{{7, outputIface, 4}}, map[uint32]outputSpec{7: {"DP-1", 1, 100, 100}})
274	obj := srv.waitBound(7)
275	srv.write(frame(obj.id, 3, appendU32(nil, 5)))
276	srv.write(frame(obj.id, 2, nil))
277	srv.closeConn()
278	timeout := time.After(10 * time.Second)
279	var err error
280	for err == nil {
281		err = c.Dispatch(nil)
282		if err != nil {
283			break
284		}
285		select {
286		case <-c.Wake():
287		case <-timeout:
288			t.Fatal("transport error never surfaced")
289		}
290	}
291	assert.Equal(t, int32(5), c.Outputs()[0].Scale, "events queued before the failure are applied first")
292}
293
294func TestHandlerCannotReenterDispatch(t *testing.T) {
295	c, srv := connectWire(t, nil, map[uint32]outputSpec{7: {"DP-1", 1, 100, 100}})
296	h := neferclientmocks.NewMockHandler(t)
297	var inner error
298	h.EXPECT().OutputAdded(mock.Anything).Run(func(*neferclient.Output) { inner = c.Dispatch(h) }).Once()
299	srv.announce(wireGlobal{7, outputIface, 4})
300	dispatchUntil(t, c, h, func() bool { return inner != nil })
301	assert.Error(t, inner)
302}
303
304func TestClose(t *testing.T) {
305	c, _ := connectWire(t, nil, nil)
306	require.NoError(t, c.Close())
307	assert.NoError(t, c.Close())
308	assert.ErrorIs(t, c.Dispatch(nil), neferclient.ErrClosed)
309	assert.ErrorIs(t, c.Roundtrip(), neferclient.ErrClosed)
310	assert.ErrorIs(t, c.WatchFD(0, 1), neferclient.ErrClosed)
311	_, _, err := c.Bind[*wayland.Shm](wayland.ShmInterface, 1, wayland.NewShm)
312	assert.ErrorIs(t, err, neferclient.ErrClosed)
313	var nilConn *neferclient.Conn
314	assert.NoError(t, nilConn.Close())
315}
316
317func TestCloseWhileReaderBlockedOnFullRing(t *testing.T) {
318	c, srv := connectWire(t, []wireGlobal{{7, outputIface, 4}}, map[uint32]outputSpec{7: {"DP-1", 1, 100, 100}})
319	obj := srv.waitBound(7)
320	var buf []byte
321	for i := 0; i < 2*neferclient.RingSize; i++ {
322		buf = append(buf, frame(obj.id, 3, appendU32(nil, 2))...)
323	}
324	go srv.write(buf)
325	waitFor(t, func() bool { return c.QueueLen() == neferclient.RingSize }, "full ring")
326	done := make(chan struct{})
327	go func() { _ = c.Close(); close(done) }()
328	select {
329	case <-done:
330	case <-time.After(5 * time.Second):
331		t.Fatal("Close hung with a producer blocked on the full ring")
332	}
333}

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 4

↑↓ moveenter openesc close