v0.6.2 f02b08f

requestargs_test.go RawHistoryFolder

1package wlturbo
2
3import (
4	"bytes"
5	"errors"
6	"net"
7	"os"
8	"strings"
9	"sync"
10	"sync/atomic"
11	"testing"
12	"time"
13
14	"golang.org/x/sys/unix"
15)
16
17// captureConn records every write so tests can compare wire bytes.
18type captureConn struct {
19	mu     sync.Mutex
20	writes [][]byte
21}
22
23func (c *captureConn) Read([]byte) (int, error) { return 0, net.ErrClosed }
24func (c *captureConn) Write(p []byte) (int, error) {
25	c.mu.Lock()
26	defer c.mu.Unlock()
27	c.writes = append(c.writes, append([]byte(nil), p...))
28	return len(p), nil
29}
30func (c *captureConn) Close() error                     { return nil }
31func (c *captureConn) LocalAddr() net.Addr              { return testAddr{} }
32func (c *captureConn) RemoteAddr() net.Addr             { return testAddr{} }
33func (c *captureConn) SetDeadline(time.Time) error      { return nil }
34func (c *captureConn) SetReadDeadline(time.Time) error  { return nil }
35func (c *captureConn) SetWriteDeadline(time.Time) error { return nil }
36
37func (c *captureConn) last(t *testing.T) []byte {
38	t.Helper()
39	c.mu.Lock()
40	defer c.mu.Unlock()
41	if len(c.writes) == 0 {
42		t.Fatal("nothing was written")
43	}
44	return c.writes[len(c.writes)-1]
45}
46
47func (c *captureConn) count() int {
48	c.mu.Lock()
49	defer c.mu.Unlock()
50	return len(c.writes)
51}
52
53func newCaptureEnv() (*Display, *captureConn, *BaseProxy) {
54	c := &captureConn{}
55	d := newDisplay(c)
56	parent := &BaseProxy{id: d.AllocateID(), context: d.context, version: 3}
57	d.context.Register(parent)
58	return d, c, parent
59}
60
61// RequestArgs and Request must put identical bytes on the wire for every
62// argument kind, including padding boundaries and nil objects.
63func TestRequestArgsMatchesBoxedRequest(t *testing.T) {
64	d, c, parent := newCaptureEnv()
65	obj := &BaseProxy{id: 77, context: d.context}
66	var nilObj Object
67	cases := []struct {
68		name  string
69		boxed []interface{}
70		typed []Arg
71	}{
72		{"none", nil, nil},
73		{"words", []interface{}{uint32(0xdeadbeef), int32(-5), Fixed(-300)},
74			[]Arg{ArgUint(0xdeadbeef), ArgInt(-5), ArgFixed(-300)}},
75		{"string_pad0", []interface{}{"abc"}, []Arg{ArgString("abc")}},
76		{"string_pad3", []interface{}{"a"}, []Arg{ArgString("a")}},
77		{"string_empty", []interface{}{""}, []Arg{ArgString("")}},
78		{"array_pad", []interface{}{[]byte{1, 2, 3, 4, 5}}, []Arg{ArgArray([]byte{1, 2, 3, 4, 5})}},
79		{"array_nil", []interface{}{[]byte(nil)}, []Arg{ArgArray(nil)}},
80		{"object", []interface{}{Object(obj)}, []Arg{ArgObject(obj)}},
81		{"nil_object", []interface{}{nilObj}, []Arg{ArgObject(nil)}},
82		{"mixed", []interface{}{uint32(9), "hello", Object(obj), int32(2), []byte("xy"), Fixed(256)},
83			[]Arg{ArgUint(9), ArgString("hello"), ArgObject(obj), ArgInt(2), ArgArray([]byte("xy")), ArgFixed(256)}},
84	}
85	for _, tc := range cases {
86		t.Run(tc.name, func(t *testing.T) {
87			if err := d.context.Request(Request{Proxy: parent, Opcode: 5, Name: "x.r"}, tc.boxed...); err != nil {
88				t.Fatal(err)
89			}
90			want := c.last(t)
91			if err := d.context.RequestArgs(Request{Proxy: parent, Opcode: 5, Name: "x.r"}, tc.typed...); err != nil {
92				t.Fatal(err)
93			}
94			got := c.last(t)
95			if !bytes.Equal(got, want) {
96				t.Fatalf("typed %x != boxed %x", got, want)
97			}
98			if len(got)%4 != 0 {
99				t.Fatalf("unaligned message: %d", len(got))
100			}
101		})
102	}
103}
104
105// Numeric words are written little-endian (native wire order on supported
106// targets) after a header carrying object, opcode and size.
107func TestRequestArgsWireLayout(t *testing.T) {
108	d, c, parent := newCaptureEnv()
109	if err := d.context.RequestArgs(Request{Proxy: parent, Opcode: 2, Name: "x.r"}, ArgUint(1), ArgInt(-1), ArgString("hi")); err != nil {
110		t.Fatal(err)
111	}
112	want := []byte{
113		byte(parent.ID()), 0, 0, 0, 2, 0, 24, 0,
114		1, 0, 0, 0,
115		0xff, 0xff, 0xff, 0xff,
116		3, 0, 0, 0, 'h', 'i', 0, 0,
117	}
118	if got := c.last(t); !bytes.Equal(got, want) {
119		t.Fatalf("got %x want %x", got, want)
120	}
121}
122
123func TestRequestArgsMalformed(t *testing.T) {
124	d, c, parent := newCaptureEnv()
125	child := &BaseProxy{context: d.context}
126	tooBig := strings.Repeat("a", 0xffff)
127	for _, tc := range []struct {
128		name string
129		args []Arg
130	}{
131		{"zero_arg", []Arg{ArgUint(1), {}}},
132		{"string_too_long", []Arg{ArgString(tooBig)}},
133		{"array_too_long", []Arg{ArgArray(make([]byte, 0xffff))}},
134		{"total_too_large", []Arg{ArgArray(make([]byte, 0xfff0)), ArgArray(make([]byte, 0x20))}},
135	} {
136		t.Run(tc.name, func(t *testing.T) {
137			before := c.count()
138			args := append([]Arg{ArgObject(child)}, tc.args...)
139			if err := d.context.RequestArgs(Request{Proxy: parent, Opcode: 1, Name: "x.bad", Child: child}, args...); err == nil {
140				t.Fatal("malformed request succeeded")
141			}
142			if c.count() != before {
143				t.Fatal("bytes written for a malformed request")
144			}
145			if _, ok := d.objects.Load(child.ID()); ok {
146				t.Fatal("child left registered after failed send")
147			}
148		})
149	}
150	// The largest 4-byte aligned message (0xfffc) still goes through.
151	if err := d.context.RequestArgs(Request{Proxy: parent, Opcode: 1, Name: "x.ok"}, ArgArray(make([]byte, 0xfffc-HeaderSize-4))); err != nil {
152		t.Fatalf("largest array: %v", err)
153	}
154	if got := c.last(t); len(got) != 0xfffc {
155		t.Fatalf("size = %d", len(got))
156	}
157}
158
159func TestRequestArgsLifecycle(t *testing.T) {
160	d, c, parent := newCaptureEnv()
161
162	// Version, child allocation and inheritance.
163	late := &BaseProxy{context: d.context}
164	err := d.context.RequestArgs(Request{Proxy: parent, Opcode: 1, Name: "x.late", Since: 4, Child: late}, ArgObject(late))
165	if !errors.Is(err, ErrVersionTooLow) {
166		t.Fatalf("since=4 on v3 = %v, want ErrVersionTooLow", err)
167	}
168	if late.ID() != 0 || c.count() != 0 {
169		t.Fatal("refused request allocated a child or wrote")
170	}
171	child := &BaseProxy{context: d.context}
172	if err := d.context.RequestArgs(Request{Proxy: parent, Opcode: 0, Name: "x.make", Child: child}, ArgObject(child)); err != nil {
173		t.Fatal(err)
174	}
175	if child.ID() == 0 || child.Version() != 3 {
176		t.Fatalf("child id=%d version=%d", child.ID(), child.Version())
177	}
178	if _, ok := d.objects.Load(child.ID()); !ok {
179		t.Fatal("child not registered")
180	}
181
182	// Destructor: claimed exactly once; later requests fail without writing.
183	if err := d.context.RequestArgs(Request{Proxy: parent, Opcode: 9, Name: "x.destroy", Destructor: true}); err != nil {
184		t.Fatal(err)
185	}
186	n := c.count()
187	if err := d.context.RequestArgs(Request{Proxy: parent, Opcode: 9, Name: "x.destroy", Destructor: true}); err == nil {
188		t.Fatal("second destructor succeeded")
189	}
190	if err := d.context.RequestArgs(Request{Proxy: parent, Opcode: 1, Name: "x.after"}, ArgUint(1)); err == nil {
191		t.Fatal("request after destructor succeeded")
192	}
193	if c.count() != n {
194		t.Fatal("bytes written after destructor")
195	}
196
197	// A marshal failure leaves a destructor's proxy registered.
198	p2 := &BaseProxy{id: d.AllocateID(), context: d.context, version: 1}
199	d.context.Register(p2)
200	if err := d.context.RequestArgs(Request{Proxy: p2, Opcode: 9, Name: "x.destroy", Destructor: true}, Arg{}); err == nil {
201		t.Fatal("zero Arg accepted")
202	}
203	if err := d.context.CheckProxy(p2); err != nil {
204		t.Fatalf("proxy lost after marshal error: %v", err)
205	}
206
207	// Foreign and closed contexts.
208	other := &BaseProxy{id: 5, context: NewContext(d)}
209	if err := d.context.RequestArgs(Request{Proxy: other, Name: "x.r"}); err == nil {
210		t.Fatal("foreign proxy accepted")
211	}
212	d.context.closed.Store(true)
213	if err := d.context.RequestArgs(Request{Proxy: p2, Name: "x.r"}); err == nil {
214		t.Fatal("closed context accepted")
215	}
216}
217
218// Descriptors are closed after a successful send and stay with the caller on
219// failure, exactly as with Request.
220func TestRequestArgsFDOwnership(t *testing.T) {
221	sp, err := unix.Socketpair(unix.AF_UNIX, unix.SOCK_STREAM|unix.SOCK_CLOEXEC, 0)
222	if err != nil {
223		t.Fatal(err)
224	}
225	fa, fb := os.NewFile(uintptr(sp[0]), "a"), os.NewFile(uintptr(sp[1]), "b")
226	ca, err := net.FileConn(fa)
227	fa.Close()
228	if err != nil {
229		t.Fatal(err)
230	}
231	cb, err := net.FileConn(fb)
232	fb.Close()
233	if err != nil {
234		t.Fatal(err)
235	}
236	defer cb.Close()
237	d, err := ConnectFromConn(ca)
238	if err != nil {
239		t.Fatal(err)
240	}
241	defer d.Close()
242	parent := &BaseProxy{id: d.AllocateID(), context: d.Context(), version: 1}
243	d.Context().Register(parent)
244
245	open := func() int {
246		f, err := os.Open(os.DevNull)
247		if err != nil {
248			t.Fatal(err)
249		}
250		fd, err := unix.Dup(int(f.Fd()))
251		f.Close()
252		if err != nil {
253			t.Fatal(err)
254		}
255		return fd
256	}
257	isOpen := func(fd int) bool { _, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0); return err == nil }
258
259	fd := open()
260	if err := d.Context().RequestArgs(Request{Proxy: parent, Opcode: 0, Name: "x.fd", FDs: []int{fd}}, ArgString("m"), ArgFD(), ArgUint(4)); err != nil {
261		t.Fatal(err)
262	}
263	if isOpen(fd) {
264		t.Fatal("descriptor not closed after successful send")
265	}
266	body := make([]byte, 8+8+4)
267	if n, oob, err := readWithOOB(cb.(*net.UnixConn), body); err != nil || n != len(body) || oob == 0 {
268		t.Fatalf("peer read n=%d oob=%d err=%v", n, oob, err)
269	}
270
271	fd = open()
272	defer unix.Close(fd)
273	if err := d.Context().RequestArgs(Request{Proxy: parent, Opcode: 0, Name: "x.fd", FDs: []int{fd}}, ArgFD(), Arg{}); err == nil {
274		t.Fatal("zero Arg accepted")
275	}
276	if !isOpen(fd) {
277		t.Fatal("descriptor closed after failed send")
278	}
279}
280
281func readWithOOB(c *net.UnixConn, buf []byte) (n, oobn int, err error) {
282	oob := make([]byte, unix.CmsgSpace(4))
283	n, oobn, _, _, err = c.ReadMsgUnix(buf, oob)
284	return
285}
286
287// The number of ArgFD markers must equal len(FDs). A mismatch is rejected
288// before the child is allocated, the guard runs or anything is written; the
289// caller keeps its descriptors and a destructor proxy stays unclaimed.
290func TestRequestArgsFDMarkerMismatch(t *testing.T) {
291	d, c, parent := newCaptureEnv()
292	fd, err := unix.Open(os.DevNull, unix.O_RDONLY|unix.O_CLOEXEC, 0)
293	if err != nil {
294		t.Fatal(err)
295	}
296	defer unix.Close(fd)
297	isOpen := func() bool { _, err := unix.FcntlInt(uintptr(fd), unix.F_GETFD, 0); return err == nil }
298
299	for _, tc := range []struct {
300		name       string
301		fds        []int
302		args       []Arg
303		destructor bool
304	}{
305		{"fd_without_marker", []int{fd}, []Arg{ArgUint(1)}, false},
306		{"fd_without_any_args", []int{fd}, nil, false},
307		{"marker_without_fd", nil, []Arg{ArgFD(), ArgUint(1)}, false},
308		{"two_markers_one_fd", []int{fd}, []Arg{ArgFD(), ArgFD()}, false},
309		{"one_marker_two_fds", []int{fd, fd}, []Arg{ArgFD()}, false},
310		{"destructor_fd_without_marker", []int{fd}, nil, true},
311		{"destructor_marker_without_fd", nil, []Arg{ArgFD()}, true},
312	} {
313		t.Run(tc.name, func(t *testing.T) {
314			before := c.count()
315			child := &BaseProxy{context: d.context}
316			args := tc.args
317			r := Request{Proxy: parent, Opcode: 1, Name: "x.fd", FDs: tc.fds, Destructor: tc.destructor}
318			if !tc.destructor {
319				r.Child = child
320				args = append([]Arg{ArgObject(child)}, args...)
321			}
322			if err := d.context.RequestArgs(r, args...); err == nil {
323				t.Fatal("mismatched fd markers accepted")
324			}
325			if c.count() != before {
326				t.Fatal("bytes written")
327			}
328			if child.ID() != 0 {
329				t.Fatal("child allocated")
330			}
331			if !isOpen() {
332				t.Fatal("caller descriptor closed")
333			}
334			if err := d.context.CheckProxy(parent); err != nil {
335				t.Fatalf("proxy claimed or lost: %v", err)
336			}
337		})
338	}
339
340	// Legacy semantics are unchanged: Request does not count placeholders.
341	if err := d.context.Request(Request{Proxy: parent, Opcode: 1, Name: "x.legacy"}, uintptr(3)); err != nil {
342		t.Fatalf("legacy uintptr placeholder without FDs: %v", err)
343	}
344}
345
346// A typed request racing a destructor on the same proxy either is written
347// before the destructor or fails without writing; nothing follows the
348// destructor on the wire.
349func TestRequestArgsConcurrentWithDestructor(t *testing.T) {
350	for i := 0; i < 50; i++ {
351		d, c, parent := newCaptureEnv()
352		var wg sync.WaitGroup
353		var ok atomic.Int32
354		for g := 0; g < 4; g++ {
355			wg.Add(1)
356			go func() {
357				defer wg.Done()
358				if d.context.RequestArgs(Request{Proxy: parent, Opcode: 1, Name: "x.r"}, ArgUint(1)) == nil {
359					ok.Add(1)
360				}
361			}()
362		}
363		var destroyed atomic.Int32
364		for g := 0; g < 2; g++ {
365			wg.Add(1)
366			go func() {
367				defer wg.Done()
368				if d.context.RequestArgs(Request{Proxy: parent, Opcode: 9, Name: "x.destroy", Destructor: true}) == nil {
369					destroyed.Add(1)
370				}
371			}()
372		}
373		wg.Wait()
374		if destroyed.Load() != 1 {
375			t.Fatalf("destructor sent %d times, want exactly 1", destroyed.Load())
376		}
377		c.mu.Lock()
378		writes := c.writes
379		c.mu.Unlock()
380		if int32(len(writes)) != ok.Load()+1 {
381			t.Fatalf("%d writes for %d requests + destructor", len(writes), ok.Load())
382		}
383		for j, w := range writes {
384			isDestructor := w[4] == 9
385			if isDestructor != (j == len(writes)-1) {
386				t.Fatalf("destructor not last on the wire: write %d of %d", j, len(writes))
387			}
388		}
389	}
390}

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 8

↑↓ moveenter openesc close