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)
16
17// writeRequest is a valid 2x2 request over a 16-byte file.
18func writeRequest(t *testing.T) ports.CaptureRequest {
19	t.Helper()
20	return pipelineRequest(t, 7)
21}
22
23// runWriteFrame calls writeFrame with a buffered reply channel and returns its
24// single reply.
25func runWriteFrame(t *testing.T, req ports.CaptureRequest, frame ports.CaptureFrame) ports.CaptureDone {
26	t.Helper()
27	replies := make(chan ports.CaptureDone, 1)
28	writeFrame(context.Background(), req, frame, time.Unix(5, 6), replies)
29	done := awaitCapture(t, replies)
30	if done.ID != req.ID {
31		t.Fatalf("reply for %d, want %d", done.ID, req.ID)
32	}
33	if len(replies) != 0 {
34		t.Fatal("more than one reply")
35	}
36	return done
37}
38
39func requireClosed(t *testing.T, f *os.File) {
40	t.Helper()
41	if f == nil {
42		return
43	}
44	if _, err := f.Stat(); err == nil {
45		t.Fatal("destination file left open")
46	}
47}
48
49func TestWriteFrameRejectsInvalidDestination(t *testing.T) {
50	for _, tc := range []struct {
51		name   string
52		mutate func(t *testing.T, q *ports.CaptureRequest)
53	}{
54		{"nil file", func(_ *testing.T, q *ports.CaptureRequest) { q.Dst.File = nil }},
55		{"zero width", func(_ *testing.T, q *ports.CaptureRequest) { q.Width = 0 }},
56		{"negative width", func(_ *testing.T, q *ports.CaptureRequest) { q.Width = -2 }},
57		{"zero height", func(_ *testing.T, q *ports.CaptureRequest) { q.Height = 0 }},
58		{"negative height", func(_ *testing.T, q *ports.CaptureRequest) { q.Height = -2 }},
59		{"stride below row", func(_ *testing.T, q *ports.CaptureRequest) { q.Stride = 4 }},
60		{"region width mismatch", func(_ *testing.T, q *ports.CaptureRequest) { q.Region = image.Rect(0, 0, 3, 2) }},
61		{"region height mismatch", func(_ *testing.T, q *ports.CaptureRequest) { q.Region = image.Rect(0, 0, 2, 1) }},
62		{"negative offset", func(_ *testing.T, q *ports.CaptureRequest) { q.Dst.Offset = -4 }},
63		{"invalid format", func(_ *testing.T, q *ports.CaptureRequest) { q.Format = 0xdeadbeef }},
64		{"width overflows bound", func(_ *testing.T, q *ports.CaptureRequest) {
65			q.Width, q.Region = math.MaxInt32, image.Rect(0, 0, math.MaxInt32, 2)
66			q.Stride = q.Width * 4
67		}},
68		{"huge stride overflowing h-1", func(_ *testing.T, q *ports.CaptureRequest) {
69			q.Height, q.Region = 3, image.Rect(0, 0, 2, 3)
70			q.Stride = math.MaxInt64 / 2
71		}},
72		{"max stride", func(_ *testing.T, q *ports.CaptureRequest) { q.Stride = math.MaxInt }},
73		{"huge height", func(_ *testing.T, q *ports.CaptureRequest) {
74			q.Height, q.Region = math.MaxInt32, image.Rect(0, 0, 2, math.MaxInt32)
75		}},
76		{"offset past bound", func(_ *testing.T, q *ports.CaptureRequest) { q.Dst.Offset = maxMappedBytes }},
77		{"one GiB plus one byte", func(_ *testing.T, q *ports.CaptureRequest) {
78			// 1 row of 4 bytes at offset 1GiB-3 ends one byte past the bound.
79			q.Width, q.Height, q.Stride, q.Region = 1, 1, 4, image.Rect(0, 0, 1, 1)
80			q.Dst.Offset = maxMappedBytes - 3
81		}},
82		{"truncated file", func(t *testing.T, q *ports.CaptureRequest) {
83			if err := q.Dst.File.Truncate(15); err != nil {
84				t.Fatal(err)
85			}
86		}},
87		{"offset past end of file", func(_ *testing.T, q *ports.CaptureRequest) { q.Dst.Offset = 4 }},
88	} {
89		t.Run(tc.name, func(t *testing.T) {
90			req := writeRequest(t)
91			tc.mutate(t, &req)
92			// No Read expectation: any Read call fails the test.
93			frame := portsmocks.NewMockCaptureFrame(t)
94			done := runWriteFrame(t, req, frame)
95			if done.Err == nil {
96				t.Fatal("invalid request succeeded")
97			}
98			requireClosed(t, req.Dst.File)
99		})
100	}
101}
102
103func TestWriteFrameAcceptsExactFit(t *testing.T) {
104	req := writeRequest(t)
105	frame := portsmocks.NewMockCaptureFrame(t)
106	frame.EXPECT().Read(req.Region, mock.Anything, req.Stride).RunAndReturn(func(_ image.Rectangle, dst []byte, _ int) error {
107		if len(dst) < 8+2*4 {
108			t.Errorf("dst holds %d bytes", len(dst))
109			return nil
110		}
111		copy(dst, []byte{1, 2, 3, 255})
112		return nil
113	}).Once()
114	done := runWriteFrame(t, req, frame)
115	if done.Err != nil {
116		t.Fatal(done.Err)
117	}
118	if !done.Time.Equal(time.Unix(5, 6)) {
119		t.Fatalf("time %v", done.Time)
120	}
121	requireClosed(t, req.Dst.File)
122	// Mockery retains Read arguments, which point at an unmapped SHM slice.
123	frame.Calls = nil
124}
125
126func TestWriteFramePropagatesReadError(t *testing.T) {
127	req := writeRequest(t)
128	boom := errors.New("readback failed")
129	frame := portsmocks.NewMockCaptureFrame(t)
130	frame.EXPECT().Read(req.Region, mock.Anything, req.Stride).Return(boom).Once()
131	done := runWriteFrame(t, req, frame)
132	if !errors.Is(done.Err, boom) {
133		t.Fatalf("err %v", done.Err)
134	}
135	requireClosed(t, req.Dst.File)
136	frame.Calls = nil
137}
138
139func TestWriteFrameRecoversFromReadPanic(t *testing.T) {
140	req := writeRequest(t)
141	frame := portsmocks.NewMockCaptureFrame(t)
142	frame.EXPECT().Read(req.Region, mock.Anything, req.Stride).RunAndReturn(func(image.Rectangle, []byte, int) error {
143		panic("renderer bug")
144	}).Once()
145	done := runWriteFrame(t, req, frame)
146	if done.Err == nil {
147		t.Fatal("panic not reported")
148	}
149	requireClosed(t, req.Dst.File)
150	frame.Calls = nil
151}
152
153// A client can truncate its buffer after the size check while it is mapped;
154// touching the mapping then raises SIGBUS, which must fail this capture only.
155func TestWriteFrameRecoversFromTruncationFault(t *testing.T) {
156	req := writeRequest(t)
157	frame := portsmocks.NewMockCaptureFrame(t)
158	frame.EXPECT().Read(req.Region, mock.Anything, req.Stride).RunAndReturn(func(_ image.Rectangle, dst []byte, _ int) error {
159		if err := req.Dst.File.Truncate(0); err != nil {
160			return err
161		}
162		dst[0] = 1 // SIGBUS: the page is past the end of the file
163		return nil
164	}).Once()
165	done := runWriteFrame(t, req, frame)
166	if done.Err == nil {
167		t.Fatal("fault not reported")
168	}
169	requireClosed(t, req.Dst.File)
170	// The compositor survives and can serve the next capture.
171	next := writeRequest(t)
172	ok := portsmocks.NewMockCaptureFrame(t)
173	ok.EXPECT().Read(next.Region, mock.Anything, next.Stride).Return(nil).Once()
174	if done := runWriteFrame(t, next, ok); done.Err != nil {
175		t.Fatalf("capture after fault: %v", done.Err)
176	}
177	frame.Calls = nil
178	ok.Calls = nil
179}

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 25

Tags 10

↑↓ moveenter openesc close