v0.1.0 47aaa0f

clipboard_extra_test.go RawHistoryFolder

1//go:build linux
2
3package wayland
4
5import (
6	"context"
7	"errors"
8	"io"
9	"net"
10	"os"
11	"testing"
12	"time"
13
14	"github.com/bnema/wlturbo/protocol/core"
15	"golang.org/x/sys/unix"
16)
17
18func TestClipboardOfferReplacement(t *testing.T) {
19	c, d, server := clipboardFixture(t)
20	first := core.NewDataOffer(d.Context())
21	first.SetID(30)
22	d.Context().Register(first)
23	second := core.NewDataOffer(d.Context())
24	second.SetID(31)
25	d.Context().Register(second)
26	for _, o := range []*core.DataOffer{first, second} {
27		c.ApplyClipboard(ClipboardEvent{kind: "offer", offer: o})
28	}
29	c.ApplyClipboard(ClipboardEvent{kind: "mime", offer: first, mime: mimePlain})
30	c.ApplyClipboard(ClipboardEvent{kind: "mime", offer: second, mime: mimeUTF8})
31	c.ApplyClipboard(ClipboardEvent{kind: "selection", offer: second})
32	if c.offer != second || c.bestMIME() != mimeUTF8 {
33		t.Fatal("latest selection not used")
34	}
35	inputRequest(t, server) // old offer destroyed
36}
37func TestClipboardWriteSendOverSocketpair(t *testing.T) {
38	c, d, server := clipboardFixture(t)
39	// WriteText sends create_data_source, three offers and set_selection.
40	if err := c.WriteText([]byte("hello")); err != nil {
41		t.Fatal(err)
42	}
43	// Client writes can coalesce into a single socket read.
44	_ = server.SetReadDeadline(time.Now().Add(20 * time.Millisecond))
45	_, _ = server.Read(make([]byte, 4096))
46	source := c.source
47	r, w, err := os.Pipe()
48	if err != nil {
49		t.Fatal(err)
50	}
51	defer r.Close()
52	defer w.Close()
53	// Wayland event string uses byte length with trailing NUL, padded to 4.
54	mime := append([]byte(mimeUTF8), 0)
55	padded := make([]byte, (len(mime)+3)&^3)
56	copy(padded, mime)
57	body := make([]byte, 4+len(padded))
58	body[0] = byte(len(mime))
59	copy(body[4:], padded)
60	msg := inputFrame(source.ID(), 1)
61	msg = append(msg, body...)
62	msg[6] = byte(len(msg))
63	msg[7] = byte(len(msg) >> 8)
64	inputSend(t, server, msg, int(w.Fd()))
65	if err = d.Dispatch(); err != nil {
66		t.Fatal(err)
67	}
68	data := make([]byte, 5)
69	_ = r.SetReadDeadline(time.Now().Add(time.Second))
70	if _, err = r.Read(data); err != nil || string(data) != "hello" {
71		t.Fatalf("send %q: %v", data, err)
72	}
73}
74func TestClipboardReadLimitAndTimeout(t *testing.T) {
75	fd := make([]int, 2)
76	if err := unix.Pipe2(fd, unix.O_CLOEXEC|unix.O_NONBLOCK); err != nil {
77		t.Fatal(err)
78	}
79	r := os.NewFile(uintptr(fd[0]), "read")
80	defer r.Close()
81	defer unix.Close(fd[1])
82	if _, err := unix.Write(fd[1], []byte("123456")); err != nil {
83		t.Fatal(err)
84	}
85	if _, err := readClipboard(context.Background(), r, 5); err == nil {
86		t.Fatal("oversize accepted")
87	}
88	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Millisecond)
89	defer cancel()
90	if _, err := readClipboard(ctx, r, 5); !errors.Is(err, context.DeadlineExceeded) {
91		t.Fatalf("timeout: %v", err)
92	}
93}
94
95func TestClipboardSendOwnershipAndCancellation(t *testing.T) {
96	c, d, server := clipboardFixture(t)
97	if err := c.WriteText([]byte("hello")); err != nil {
98		t.Fatal(err)
99	}
100	inputRequest(t, server)
101	source := c.source
102	for _, mime := range []string{mimeUTF8, "application/octet-stream"} {
103		r, w, err := os.Pipe()
104		if err != nil {
105			t.Fatal(err)
106		}
107		data := append([]byte(mime), 0)
108		padded := make([]byte, (len(data)+3)&^3)
109		copy(padded, data)
110		msg := inputFrame(source.ID(), 1, uint32(len(data)))
111		msg = append(msg, padded...)
112		msg[6], msg[7] = byte(len(msg)), byte(len(msg)>>8)
113		inputSend(t, server, msg, int(w.Fd()))
114		if err = d.Dispatch(); err != nil {
115			t.Fatal(err)
116		}
117		_ = w.Close()
118		_ = r.SetReadDeadline(time.Now().Add(time.Second))
119		buf := make([]byte, 8)
120		n, err := r.Read(buf)
121		if err != nil && err != io.EOF {
122			t.Fatal(err)
123		}
124		if mime == mimeUTF8 && string(buf[:n]) != "hello" || mime != mimeUTF8 && n != 0 {
125			t.Fatalf("send %s: %q %v", mime, buf[:n], err)
126		}
127		_ = r.Close()
128	}
129	inputSend(t, server, inputFrame(source.ID(), 2))
130	if err := d.Dispatch(); err != nil {
131		t.Fatal(err)
132	}
133	if c.source != nil {
134		t.Fatal("cancelled source retained")
135	}
136	inputRequest(t, server) // source.destroy
137}
138
139func TestClipboardSelectionKeepsDnD(t *testing.T) {
140	c, d, server := clipboardFixture(t)
141	drag := core.NewDataOffer(d.Context())
142	drag.SetID(70)
143	d.Context().Register(drag)
144	selected := core.NewDataOffer(d.Context())
145	selected.SetID(71)
146	d.Context().Register(selected)
147	c.ApplyClipboard(ClipboardEvent{kind: "offer", offer: drag})
148	c.ApplyClipboard(ClipboardEvent{kind: "enter", offer: drag})
149	c.ApplyClipboard(ClipboardEvent{kind: "offer", offer: selected})
150	c.ApplyClipboard(ClipboardEvent{kind: "selection", offer: selected})
151	if c.dnd != drag || c.offer != selected {
152		t.Fatal("lost active drag or selection")
153	}
154	_ = server.SetReadDeadline(time.Now().Add(20 * time.Millisecond))
155	if _, err := server.Read(make([]byte, 128)); err == nil {
156		t.Fatal("active drag destroyed")
157	}
158	c.ApplyClipboard(ClipboardEvent{kind: "drop"})
159	if c.dnd != nil {
160		t.Fatal("drop retained drag")
161	}
162	inputRequest(t, server)
163}
164
165func TestClipboardCloseCancelsReadWithoutDelivery(t *testing.T) {
166	c, d, server := clipboardFixture(t)
167	offer := core.NewDataOffer(d.Context())
168	offer.SetID(80)
169	d.Context().Register(offer)
170	c.ApplyClipboard(ClipboardEvent{kind: "offer", offer: offer})
171	c.ApplyClipboard(ClipboardEvent{kind: "mime", offer: offer, mime: mimePlain})
172	c.ApplyClipboard(ClipboardEvent{kind: "selection", offer: offer})
173	delivered := make(chan struct{}, 1)
174	if err := c.ReadTextAsync(context.Background(), 32, func([]byte, error) { delivered <- struct{}{} }); err != nil {
175		t.Fatal(err)
176	}
177	// Keep the compositor's write end open so the read cannot finish before Close.
178	buf, oob := make([]byte, 256), make([]byte, 128)
179	_ = server.SetReadDeadline(time.Now().Add(time.Second))
180	_, n, _, _, err := server.ReadMsgUnix(buf, oob)
181	if err != nil {
182		t.Fatal(err)
183	}
184	msgs, err := unix.ParseSocketControlMessage(oob[:n])
185	if err != nil {
186		t.Fatal(err)
187	}
188	for _, msg := range msgs {
189		fds, err := unix.ParseUnixRights(&msg)
190		if err != nil {
191			t.Fatal(err)
192		}
193		for _, fd := range fds {
194			defer unix.Close(fd)
195		}
196	}
197	done := make(chan struct{})
198	go func() { c.Close(); close(done) }()
199	select {
200	case <-done:
201	case <-time.After(time.Second):
202		t.Fatal("Close did not wait for cancelled read")
203	}
204	select {
205	case <-delivered:
206		t.Fatal("delivered after Close")
207	default:
208	}
209}
210
211func TestClipboardCloseInsideDelivery(t *testing.T) {
212	c, d, server := clipboardFixture(t)
213	offer := core.NewDataOffer(d.Context())
214	offer.SetID(81)
215	d.Context().Register(offer)
216	c.ApplyClipboard(ClipboardEvent{kind: "offer", offer: offer})
217	c.ApplyClipboard(ClipboardEvent{kind: "mime", offer: offer, mime: mimePlain})
218	c.ApplyClipboard(ClipboardEvent{kind: "selection", offer: offer})
219	done := make(chan struct{})
220	if err := c.ReadTextAsync(context.Background(), 32, func([]byte, error) { c.Close(); close(done) }); err != nil {
221		t.Fatal(err)
222	}
223	clipboardFinishRead(t, server)
224	select {
225	case <-done:
226	case <-time.After(time.Second):
227		t.Fatal("Close deadlocked in delivery")
228	}
229}
230
231func TestClipboardCloseWhileDeliveryBlocks(t *testing.T) {
232	c, d, server := clipboardFixture(t)
233	offer := core.NewDataOffer(d.Context())
234	offer.SetID(82)
235	d.Context().Register(offer)
236	c.ApplyClipboard(ClipboardEvent{kind: "offer", offer: offer})
237	c.ApplyClipboard(ClipboardEvent{kind: "mime", offer: offer, mime: mimePlain})
238	c.ApplyClipboard(ClipboardEvent{kind: "selection", offer: offer})
239	entered := make(chan struct{})
240	unblock := make(chan struct{})
241	callbackDone := make(chan struct{})
242	if err := c.ReadTextAsync(context.Background(), 32, func([]byte, error) {
243		close(entered)
244		<-unblock
245		close(callbackDone)
246	}); err != nil {
247		t.Fatal(err)
248	}
249	clipboardFinishRead(t, server)
250	select {
251	case <-entered:
252	case <-time.After(time.Second):
253		t.Fatal("delivery not called")
254	}
255	done := make(chan struct{})
256	go func() { c.Close(); close(done) }()
257	select {
258	case <-done:
259	case <-time.After(time.Second):
260		t.Fatal("Close blocked on delivery")
261	}
262	close(unblock)
263	select {
264	case <-callbackDone:
265	case <-time.After(time.Second):
266		t.Fatal("callback did not finish")
267	}
268}
269
270// Consume receive and close the transferred write FD to complete the read.
271func clipboardFinishRead(t *testing.T, server *net.UnixConn) {
272	t.Helper()
273	buf, oob := make([]byte, 256), make([]byte, 128)
274	_ = server.SetReadDeadline(time.Now().Add(time.Second))
275	_, n, _, _, err := server.ReadMsgUnix(buf, oob)
276	if err != nil {
277		t.Fatal(err)
278	}
279	msgs, err := unix.ParseSocketControlMessage(oob[:n])
280	if err != nil {
281		t.Fatal(err)
282	}
283	for _, m := range msgs {
284		fds, err := unix.ParseUnixRights(&m)
285		if err != nil {
286			t.Fatal(err)
287		}
288		for _, fd := range fds {
289			_, _ = unix.Write(fd, []byte("ok"))
290			_ = unix.Close(fd)
291		}
292	}
293}

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 12

↑↓ moveenter openesc close