feat/hints a471fa4

write_test.go RawHistoryFolder

1package capture
2
3import (
4	"context"
5	"errors"
6	"image"
7	"math"
8	"os"
9	"testing"
10	"time"
11
12	portsmocks "github.com/bnema/neferwl/internal/mocks/ports"
13	"github.com/bnema/neferwl/internal/ports"
14	"github.com/stretchr/testify/mock"
15	"golang.org/x/sys/unix"
16)
17
18// writeRequest is a valid 2x2 request over a 16-byte file.
19func writeRequest(t *testing.T) ports.CaptureRequest {
20	t.Helper()
21	return pipelineRequest(t, 7)
22}
23
24// runWriteFrame calls writeFrame with a buffered reply channel and returns its
25// single reply.
26func runWriteFrame(t *testing.T, req ports.CaptureRequest, frame ports.CaptureFrame) ports.CaptureDone {
27	t.Helper()
28	replies := make(chan ports.CaptureDone, 1)
29	writeFrame(context.Background(), req, frame, time.Unix(5, 6), replies)
30	done := awaitCapture(t, replies)
31	if done.ID != req.ID {
32		t.Fatalf("reply for %d, want %d", done.ID, req.ID)
33	}
34	if len(replies) != 0 {
35		t.Fatal("more than one reply")
36	}
37	return done
38}
39
40func requireClosed(t *testing.T, f *os.File) {
41	t.Helper()
42	if f == nil {
43		return
44	}
45	if _, err := f.Stat(); err == nil {
46		t.Fatal("destination file left open")
47	}
48}
49
50func TestWriteFrameRejectsInvalidDestination(t *testing.T) {
51	for _, tc := range []struct {
52		name   string
53		mutate func(t *testing.T, q *ports.CaptureRequest)
54	}{
55		{"nil file", func(_ *testing.T, q *ports.CaptureRequest) { q.Dst.File = nil }},
56		{"zero width", func(_ *testing.T, q *ports.CaptureRequest) { q.Width = 0 }},
57		{"negative width", func(_ *testing.T, q *ports.CaptureRequest) { q.Width = -2 }},
58		{"zero height", func(_ *testing.T, q *ports.CaptureRequest) { q.Height = 0 }},
59		{"negative height", func(_ *testing.T, q *ports.CaptureRequest) { q.Height = -2 }},
60		{"stride below row", func(_ *testing.T, q *ports.CaptureRequest) { q.Stride = 4 }},
61		{"region width mismatch", func(_ *testing.T, q *ports.CaptureRequest) { q.Region = image.Rect(0, 0, 3, 2) }},
62		{"region height mismatch", func(_ *testing.T, q *ports.CaptureRequest) { q.Region = image.Rect(0, 0, 2, 1) }},
63		{"negative offset", func(_ *testing.T, q *ports.CaptureRequest) { q.Dst.Offset = -4 }},
64		{"invalid format", func(_ *testing.T, q *ports.CaptureRequest) { q.Format = 0xdeadbeef }},
65		{"width overflows bound", func(_ *testing.T, q *ports.CaptureRequest) {
66			q.Width, q.Region = math.MaxInt32, image.Rect(0, 0, math.MaxInt32, 2)
67			q.Stride = q.Width * 4
68		}},
69		{"huge stride overflowing h-1", func(_ *testing.T, q *ports.CaptureRequest) {
70			q.Height, q.Region = 3, image.Rect(0, 0, 2, 3)
71			q.Stride = math.MaxInt64 / 2
72		}},
73		{"max stride", func(_ *testing.T, q *ports.CaptureRequest) { q.Stride = math.MaxInt }},
74		{"huge height", func(_ *testing.T, q *ports.CaptureRequest) {
75			q.Height, q.Region = math.MaxInt32, image.Rect(0, 0, 2, math.MaxInt32)
76		}},
77		{"offset past bound", func(_ *testing.T, q *ports.CaptureRequest) { q.Dst.Offset = maxMappedBytes }},
78		{"one GiB plus one byte", func(_ *testing.T, q *ports.CaptureRequest) {
79			// 1 row of 4 bytes at offset 1GiB-3 ends one byte past the bound.
80			q.Width, q.Height, q.Stride, q.Region = 1, 1, 4, image.Rect(0, 0, 1, 1)
81			q.Dst.Offset = maxMappedBytes - 3
82		}},
83		{"truncated file", func(t *testing.T, q *ports.CaptureRequest) {
84			if err := q.Dst.File.Truncate(15); err != nil {
85				t.Fatal(err)
86			}
87		}},
88		{"offset past end of file", func(_ *testing.T, q *ports.CaptureRequest) { q.Dst.Offset = 4 }},
89	} {
90		t.Run(tc.name, func(t *testing.T) {
91			req := writeRequest(t)
92			tc.mutate(t, &req)
93			// No Read expectation: any Read call fails the test.
94			frame := portsmocks.NewMockCaptureFrame(t)
95			done := runWriteFrame(t, req, frame)
96			if done.Err == nil {
97				t.Fatal("invalid request succeeded")
98			}
99			requireClosed(t, req.Dst.File)
100		})
101	}
102}
103
104func TestWriteFrameAcceptsExactFit(t *testing.T) {
105	req := writeRequest(t)
106	frame := portsmocks.NewMockCaptureFrame(t)
107	frame.EXPECT().Read(req.Region, mock.Anything, req.Stride).RunAndReturn(func(_ image.Rectangle, dst []byte, _ int) error {
108		if len(dst) < 8+2*4 {
109			t.Errorf("dst holds %d bytes", len(dst))
110			return nil
111		}
112		copy(dst, []byte{1, 2, 3, 255})
113		return nil
114	}).Once()
115	done := runWriteFrame(t, req, frame)
116	if done.Err != nil {
117		t.Fatal(done.Err)
118	}
119	if !done.Time.Equal(time.Unix(5, 6)) {
120		t.Fatalf("time %v", done.Time)
121	}
122	requireClosed(t, req.Dst.File)
123	// Mockery retains Read arguments, which point at an unmapped SHM slice.
124	frame.Calls = nil
125}
126
127func TestWriteFramePropagatesReadError(t *testing.T) {
128	req := writeRequest(t)
129	boom := errors.New("readback failed")
130	frame := portsmocks.NewMockCaptureFrame(t)
131	frame.EXPECT().Read(req.Region, mock.Anything, req.Stride).Return(boom).Once()
132	done := runWriteFrame(t, req, frame)
133	if !errors.Is(done.Err, boom) {
134		t.Fatalf("err %v", done.Err)
135	}
136	requireClosed(t, req.Dst.File)
137	frame.Calls = nil
138}
139
140func TestWriteFrameRecoversFromReadPanic(t *testing.T) {
141	req := writeRequest(t)
142	frame := portsmocks.NewMockCaptureFrame(t)
143	frame.EXPECT().Read(req.Region, mock.Anything, req.Stride).RunAndReturn(func(image.Rectangle, []byte, int) error {
144		panic("renderer bug")
145	}).Once()
146	done := runWriteFrame(t, req, frame)
147	if done.Err == nil {
148		t.Fatal("panic not reported")
149	}
150	requireClosed(t, req.Dst.File)
151	frame.Calls = nil
152}
153
154// A client can truncate its buffer after the size check while it is mapped;
155// touching the mapping then raises SIGBUS, which must fail this capture only.
156func TestWriteFrameRecoversFromTruncationFault(t *testing.T) {
157	req := writeRequest(t)
158	frame := portsmocks.NewMockCaptureFrame(t)
159	frame.EXPECT().Read(req.Region, mock.Anything, req.Stride).RunAndReturn(func(_ image.Rectangle, dst []byte, _ int) error {
160		if err := req.Dst.File.Truncate(0); err != nil {
161			return err
162		}
163		dst[0] = 1 // SIGBUS: the page is past the end of the file
164		return nil
165	}).Once()
166	done := runWriteFrame(t, req, frame)
167	if done.Err == nil {
168		t.Fatal("fault not reported")
169	}
170	requireClosed(t, req.Dst.File)
171	// The compositor survives and can serve the next capture.
172	next := writeRequest(t)
173	ok := portsmocks.NewMockCaptureFrame(t)
174	ok.EXPECT().Read(next.Region, mock.Anything, next.Stride).Return(nil).Once()
175	if done := runWriteFrame(t, next, ok); done.Err != nil {
176		t.Fatalf("capture after fault: %v", done.Err)
177	}
178	frame.Calls = nil
179	ok.Calls = nil
180}
181
182// mappedRequest is a valid 2x2 request that writes into a borrowed mapping of
183// size bytes.
184func mappedRequest(size int) ports.CaptureRequest {
185	return ports.CaptureRequest{ID: 9, Region: image.Rect(0, 0, 2, 2), Width: 2, Height: 2, Stride: 8, Format: 1, Mapped: make([]byte, size)}
186}
187
188func TestWriteFrameMappedWritesAtOffset(t *testing.T) {
189	req := mappedRequest(32)
190	req.Dst.Offset = 8
191	frame := portsmocks.NewMockCaptureFrame(t)
192	frame.EXPECT().Read(req.Region, mock.Anything, req.Stride).RunAndReturn(func(_ image.Rectangle, dst []byte, _ int) error {
193		if len(dst) != 24 {
194			t.Errorf("dst holds %d bytes, want 24 (mapping from offset 8)", len(dst))
195			return nil
196		}
197		copy(dst, []byte{1, 2, 3, 255})
198		return nil
199	}).Once()
200	done := runWriteFrame(t, req, frame)
201	if done.Err != nil {
202		t.Fatal(done.Err)
203	}
204	if got := req.Mapped[8:12]; got[0] != 1 || got[3] != 255 {
205		t.Fatalf("mapping not written at the offset: %v", req.Mapped[:16])
206	}
207	frame.Calls = nil
208}
209
210func TestWriteFrameMappedExactFit(t *testing.T) {
211	req := mappedRequest(8 + 2*4) // (h-1)*stride + w*4
212	frame := portsmocks.NewMockCaptureFrame(t)
213	frame.EXPECT().Read(req.Region, mock.Anything, req.Stride).Return(nil).Once()
214	if done := runWriteFrame(t, req, frame); done.Err != nil {
215		t.Fatal(done.Err)
216	}
217	frame.Calls = nil
218}
219
220func TestWriteFrameMappedRejectsShortOrAmbiguous(t *testing.T) {
221	for _, tc := range []struct {
222		name   string
223		mutate func(q *ports.CaptureRequest)
224	}{
225		{"one byte short", func(q *ports.CaptureRequest) { q.Mapped = q.Mapped[:15] }},
226		{"offset past mapping", func(q *ports.CaptureRequest) { q.Dst.Offset = 4 }},
227		{"stride bound", func(q *ports.CaptureRequest) { q.Stride = math.MaxInt }},
228		{"negative offset", func(q *ports.CaptureRequest) { q.Dst.Offset = -4 }},
229		{"invalid format", func(q *ports.CaptureRequest) { q.Format = 7 }},
230		{"file and mapping", func(q *ports.CaptureRequest) { q.Dst.File = os.Stdin }},
231	} {
232		t.Run(tc.name, func(t *testing.T) {
233			req := mappedRequest(16)
234			tc.mutate(&req)
235			frame := portsmocks.NewMockCaptureFrame(t) // any Read call fails
236			if done := runWriteFrame(t, req, frame); done.Err == nil {
237				t.Fatal("invalid request succeeded")
238			}
239		})
240	}
241}
242
243// A borrowed mapping is never closed or unmapped by the output: it stays
244// writable after the reply.
245func TestWriteFrameMappedStaysMapped(t *testing.T) {
246	m, err := unix.Mmap(-1, 0, 4096, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_ANON|unix.MAP_PRIVATE)
247	if err != nil {
248		t.Fatal(err)
249	}
250	defer func() { _ = unix.Munmap(m) }()
251	req := mappedRequest(0)
252	req.Mapped = m
253	frame := portsmocks.NewMockCaptureFrame(t)
254	frame.EXPECT().Read(req.Region, mock.Anything, req.Stride).Return(nil).Once()
255	if done := runWriteFrame(t, req, frame); done.Err != nil {
256		t.Fatal(done.Err)
257	}
258	m[0], m[4095] = 1, 2 // would fault if unmapped
259	frame.Calls = nil
260}
261
262func TestHandedKeepsMappedRequests(t *testing.T) {
263	if Handed(ports.CaptureRequest{Mapped: make([]byte, 4)}) {
264		t.Fatal("a mapped request with ID 0 is not handed")
265	}
266	if !Handed(ports.CaptureRequest{}) {
267		t.Fatal("a zero request is handed")
268	}
269}
270
271// The measured closure holds the production steps of a mapped write without
272// the renderer's Read (a testify mock would record each call): destination
273// validation, the borrowed slice and the reply.
274func TestWriteMappedAllocations(t *testing.T) {
275	if raceEnabled {
276		t.Skip("the race detector allocates")
277	}
278	req := mappedRequest(16)
279	replies := make(chan ports.CaptureDone, 1)
280	ctx := context.Background()
281	at := time.Unix(1, 2)
282	var sink []byte
283	allocs := testing.AllocsPerRun(200, func() {
284		size, err := destSize(req)
285		if err != nil {
286			panic(err)
287		}
288		sink, err = mappedDst(req, size)
289		if err != nil {
290			panic(err)
291		}
292		replyAt(ctx, req, nil, at, replies)
293		<-replies
294	})
295	if allocs != 0 || len(sink) != 16 {
296		t.Fatalf("%v allocs per mapped write step, dst %d bytes", allocs, len(sink))
297	}
298}

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 23

Tags 10

↑↓ moveenter openesc close