v0.4.0 5f1ad00

wire_test.go RawHistoryFolder

1package neferclient_test
2
3import (
4	"context"
5	"encoding/binary"
6	"errors"
7	"io"
8	"net"
9	"os"
10	"sync"
11	"testing"
12	"time"
13
14	"github.com/bnema/go-wayland-bindings/client/wayland"
15	"github.com/bnema/neferclient"
16	"golang.org/x/sys/unix"
17)
18
19// wireServer speaks just enough of the compositor side of the Wayland wire
20// protocol over a socketpair to drive Conn: registry globals, sync callbacks,
21// wl_output binds and caller-scripted events. It is the protocol peer, not a
22// double of one of our own interfaces.
23type wireServer struct {
24	t    *testing.T
25	conn *net.UnixConn
26
27	mu       sync.Mutex
28	cond     *sync.Cond
29	registry uint32
30	globals  []wireGlobal
31	bound    map[uint32]boundObject // global name -> bound object
32	outputs  map[uint32]outputSpec
33	hook     func(obj uint32, op uint16, body []byte) // other requests, if set
34}
35
36type wireGlobal struct {
37	name    uint32
38	iface   string
39	version uint32
40}
41
42type boundObject struct {
43	id      uint32
44	version uint32
45}
46
47// outputSpec is what the server sends right after an output bind.
48type outputSpec struct {
49	name          string
50	scale         int32
51	width, height int32
52}
53
54func appendU32(b []byte, v uint32) []byte { return binary.LittleEndian.AppendUint32(b, v) }
55
56func appendStr(b []byte, s string) []byte {
57	n := len(s) + 1
58	b = appendU32(b, uint32(n))
59	b = append(b, s...)
60	b = append(b, 0)
61	for len(b)%4 != 0 {
62		b = append(b, 0)
63	}
64	return b
65}
66
67func frame(obj uint32, op uint16, body []byte) []byte {
68	b := appendU32(nil, obj)
69	b = appendU32(b, uint32(8+len(body))<<16|uint32(op))
70	return append(b, body...)
71}
72
73// socketPair returns the client end as a net.Conn and the server end.
74func socketPair(t *testing.T) (net.Conn, *net.UnixConn) {
75	t.Helper()
76	fds, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
77	if err != nil {
78		t.Fatal(err)
79	}
80	mk := func(fd int) net.Conn {
81		f := os.NewFile(uintptr(fd), "socketpair")
82		c, err := net.FileConn(f)
83		_ = f.Close()
84		if err != nil {
85			t.Fatal(err)
86		}
87		return c
88	}
89	client, server := mk(fds[0]), mk(fds[1])
90	t.Cleanup(func() { _ = server.Close() })
91	return client, server.(*net.UnixConn)
92}
93
94// startWireServer serves a socketpair. outputs maps an announced wl_output
95// global to the description sent when it is bound.
96func startWireServer(t *testing.T, globals []wireGlobal, outputs map[uint32]outputSpec) (net.Conn, *wireServer) {
97	t.Helper()
98	client, srvConn := socketPair(t)
99	s := &wireServer{t: t, conn: srvConn, globals: globals, outputs: outputs,
100		bound: map[uint32]boundObject{}}
101	s.cond = sync.NewCond(&s.mu)
102	go s.serve()
103	return client, s
104}
105
106func (s *wireServer) write(b []byte) {
107	if _, err := s.conn.Write(b); err != nil && !isClosed(err) {
108		s.t.Errorf("wire server write: %v", err)
109	}
110}
111
112func isClosed(err error) bool {
113	return errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed) || errors.Is(err, os.ErrDeadlineExceeded) ||
114		errors.Is(err, unix.EPIPE) || errors.Is(err, unix.ECONNRESET)
115}
116
117func (s *wireServer) serve() {
118	var hdr [8]byte
119	var buf []byte // reused: the peer must not allocate while draining
120	for {
121		if _, err := io.ReadFull(s.conn, hdr[:]); err != nil {
122			return
123		}
124		obj := binary.LittleEndian.Uint32(hdr[0:4])
125		w := binary.LittleEndian.Uint32(hdr[4:8])
126		size, op := int(w>>16), uint16(w)
127		if cap(buf) < size-8 {
128			buf = make([]byte, size-8)
129		}
130		body := buf[:size-8]
131		if _, err := io.ReadFull(s.conn, body); err != nil {
132			return
133		}
134		s.handle(obj, op, body)
135	}
136}
137
138func (s *wireServer) handle(obj uint32, op uint16, body []byte) {
139	s.mu.Lock()
140	reg := s.registry
141	hook := s.hook
142	s.mu.Unlock()
143	if hook != nil && obj != 1 && obj != reg {
144		hook(obj, op, body) // every request on a client-created or bound object
145	}
146	switch {
147	case obj == 1 && op == 1: // get_registry
148		id := binary.LittleEndian.Uint32(body)
149		s.mu.Lock()
150		s.registry = id
151		globals := s.globals
152		s.mu.Unlock()
153		for _, g := range globals {
154			s.announce(g)
155		}
156	case obj == 1 && op == 0: // sync
157		id := binary.LittleEndian.Uint32(body)
158		s.write(frame(id, 0, appendU32(nil, 0)))
159		s.write(frame(1, 1, appendU32(nil, id)))
160		s.mu.Lock()
161		s.cond.Broadcast()
162		s.mu.Unlock()
163	case obj == reg && op == 0: // bind
164		name := binary.LittleEndian.Uint32(body)
165		n := int(binary.LittleEndian.Uint32(body[4:]))
166		off := 8 + (n+3)&^3
167		version := binary.LittleEndian.Uint32(body[off:])
168		id := binary.LittleEndian.Uint32(body[off+4:])
169		s.mu.Lock()
170		s.bound[name] = boundObject{id: id, version: version}
171		spec, isOutput := s.outputs[name]
172		s.cond.Broadcast()
173		s.mu.Unlock()
174		if isOutput {
175			s.describe(id, version, spec)
176		}
177	case op == 0 && len(body) == 0: // wl_output.release (destructor, no args)
178		s.write(frame(1, 1, appendU32(nil, obj)))
179	}
180}
181
182func (s *wireServer) announce(g wireGlobal) {
183	b := appendU32(nil, g.name)
184	b = appendStr(b, g.iface)
185	b = appendU32(b, g.version)
186	s.mu.Lock()
187	reg := s.registry
188	s.mu.Unlock()
189	s.write(frame(reg, 0, b))
190}
191
192func (s *wireServer) remove(name uint32) {
193	s.mu.Lock()
194	reg := s.registry
195	s.mu.Unlock()
196	s.write(frame(reg, 1, appendU32(nil, name)))
197}
198
199// describe sends mode, scale, name (version >= 4) and done for an output.
200func (s *wireServer) describe(id, version uint32, o outputSpec) {
201	mode := appendU32(nil, 3) // current|preferred
202	mode = appendU32(mode, uint32(o.width))
203	mode = appendU32(mode, uint32(o.height))
204	mode = appendU32(mode, 60000)
205	s.write(frame(id, 1, mode))
206	s.write(frame(id, 3, appendU32(nil, uint32(o.scale))))
207	if version >= 4 {
208		s.write(frame(id, 4, appendStr(nil, o.name)))
209	}
210	s.write(frame(id, 2, nil))
211}
212
213// waitBound waits until the client bound global and returns the object.
214func (s *wireServer) waitBound(global uint32) boundObject {
215	s.t.Helper()
216	deadline := time.AfterFunc(5*time.Second, func() {
217		s.mu.Lock()
218		s.cond.Broadcast()
219		s.mu.Unlock()
220	})
221	defer deadline.Stop()
222	start := time.Now()
223	s.mu.Lock()
224	defer s.mu.Unlock()
225	for {
226		if b, ok := s.bound[global]; ok {
227			return b
228		}
229		if time.Since(start) > 5*time.Second {
230			s.t.Fatalf("global %d never bound", global)
231		}
232		s.cond.Wait()
233	}
234}
235
236func (s *wireServer) closeConn() { _ = s.conn.Close() }
237
238// connectWire connects a Conn to the wire server.
239func connectWire(t *testing.T, globals []wireGlobal, outputs map[uint32]outputSpec) (*neferclient.Conn, *wireServer) {
240	t.Helper()
241	client, srv := startWireServer(t, globals, outputs)
242	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
243	t.Cleanup(cancel)
244	c, err := neferclient.ConnectConn(ctx, client)
245	if err != nil {
246		t.Fatalf("connect: %v", err)
247	}
248	t.Cleanup(func() { _ = c.Close() })
249	return c, srv
250}
251
252const outputIface = wayland.OutputInterface

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