batchwriter_test.go RawHistoryFolder

1package term
2
3import (
4	"errors"
5	"testing"
6
7	"github.com/bnema/vev/internal/domain"
8)
9
10// recordingWriter records each Write call it receives, copying the bytes
11// so later mutations by the caller (e.g. buffer reuse) don't corrupt the
12// recorded history.
13type recordingWriter struct {
14	writes   [][]byte
15	failNext bool
16}
17
18func (r *recordingWriter) Write(p []byte) (int, error) {
19	if r.failNext {
20		r.failNext = false
21		return 0, errors.New("boom")
22	}
23	cp := make([]byte, len(p))
24	copy(cp, p)
25	r.writes = append(r.writes, cp)
26	return len(p), nil
27}
28
29func (r *recordingWriter) all() []byte {
30	var out []byte
31	for _, w := range r.writes {
32		out = append(out, w...)
33	}
34	return out
35}
36
37type recordingObservation struct {
38	writes      [][]byte
39	flushes     int
40	invalidated bool
41}
42
43func (r *recordingObservation) ObserveTerminalWrite(data []byte) {
44	r.writes = append(r.writes, append([]byte(nil), data...))
45}
46func (r *recordingObservation) ObserveTerminalFlush()                 { r.flushes++ }
47func (r *recordingObservation) ObserveTerminalResize(domain.Geometry) {}
48func (r *recordingObservation) InvalidateTerminalObservation()        { r.invalidated = true }
49
50func TestBatchWriter_BufferedUntilFlush(t *testing.T) {
51	rw := &recordingWriter{}
52	bw := newBatchWriter(rw, 64)
53
54	n, err := bw.Write([]byte("hello"))
55	if err != nil || n != 5 {
56		t.Fatalf("Write() = %d, %v; want 5, nil", n, err)
57	}
58	if len(rw.writes) != 0 {
59		t.Fatalf("expected no writes reaching sink before Flush, got %d", len(rw.writes))
60	}
61	if got := bw.Buffered(); got != 5 {
62		t.Fatalf("Buffered() = %d, want 5", got)
63	}
64
65	if err := bw.Flush(); err != nil {
66		t.Fatalf("Flush() = %v, want nil", err)
67	}
68	if got := string(rw.all()); got != "hello" {
69		t.Fatalf("sink got %q, want %q", got, "hello")
70	}
71	if got := bw.Buffered(); got != 0 {
72		t.Fatalf("Buffered() after Flush = %d, want 0", got)
73	}
74}
75
76func TestBatchWriter_MultipleWritesCoalesceUntilFlush(t *testing.T) {
77	rw := &recordingWriter{}
78	bw := newBatchWriter(rw, 64)
79
80	for _, s := range []string{"a", "b", "c"} {
81		if _, err := bw.WriteString(s); err != nil {
82			t.Fatalf("WriteString(%q): %v", s, err)
83		}
84	}
85	if len(rw.writes) != 0 {
86		t.Fatalf("expected 0 writes to sink before Flush, got %d", len(rw.writes))
87	}
88
89	if err := bw.Flush(); err != nil {
90		t.Fatalf("Flush(): %v", err)
91	}
92	if got := string(rw.all()); got != "abc" {
93		t.Fatalf("sink got %q, want %q", got, "abc")
94	}
95	// bufio.Writer.Flush issues a single underlying Write.
96	if len(rw.writes) != 1 {
97		t.Fatalf("expected exactly 1 write to sink on Flush, got %d", len(rw.writes))
98	}
99}
100
101func TestBatchWriter_LargeWritePassesThrough(t *testing.T) {
102	rw := &recordingWriter{}
103	bw := newBatchWriter(rw, 8) // small buffer
104
105	big := make([]byte, 64)
106	for i := range big {
107		big[i] = 'x'
108	}
109
110	n, err := bw.Write(big)
111	if err != nil || n != len(big) {
112		t.Fatalf("Write(big) = %d, %v; want %d, nil", n, err, len(big))
113	}
114	// A write larger than the buffer bypasses buffering entirely.
115	if len(rw.writes) == 0 {
116		t.Fatalf("expected large write to pass through immediately, got 0 sink writes")
117	}
118	if got := string(rw.all()); got != string(big) {
119		t.Fatalf("sink content mismatch: got %d bytes, want %d bytes", len(got), len(big))
120	}
121}
122
123func TestBatchWriter_FlushPropagatesError(t *testing.T) {
124	rw := &recordingWriter{failNext: true}
125	bw := newBatchWriter(rw, 64)
126
127	if _, err := bw.WriteString("x"); err != nil {
128		t.Fatalf("WriteString: %v", err)
129	}
130	if err := bw.Flush(); err == nil {
131		t.Fatalf("expected Flush error, got nil")
132	}
133}
134
135func TestBatchWriter_FlushOnEmptyBufferIsNoop(t *testing.T) {
136	rw := &recordingWriter{}
137	bw := newBatchWriter(rw, 64)
138
139	if err := bw.Flush(); err != nil {
140		t.Fatalf("Flush() on empty buffer: %v", err)
141	}
142	if len(rw.writes) != 0 {
143		t.Fatalf("expected no sink writes, got %d", len(rw.writes))
144	}
145}
146
147func TestBatchWriter_ObservesOnlySuccessfulPhysicalWrites(t *testing.T) {
148	rw := &recordingWriter{}
149	observation := &recordingObservation{}
150	bw := newBatchWriterWithObservation(rw, 4, observation)
151
152	if _, err := bw.WriteString("abc"); err != nil {
153		t.Fatal(err)
154	}
155	if _, err := bw.WriteString("defgh"); err != nil {
156		t.Fatal(err)
157	}
158	if got := string(observation.writes[0]); got != "abcd" {
159		t.Fatalf("observed auto-flush prefix = %q", got)
160	}
161	if observation.flushes != 0 {
162		t.Fatal("auto-flush ended a publication transaction")
163	}
164	if err := bw.Flush(); err != nil {
165		t.Fatal(err)
166	}
167	if observation.flushes != 1 {
168		t.Fatalf("flush boundaries = %d, want 1", observation.flushes)
169	}
170	if got := string(append(observation.writes[0], observation.writes[1]...)); got != "abcdefgh" {
171		t.Fatalf("observed successful prefixes = %q", got)
172	}
173
174	rw.failNext = true
175	if _, err := bw.WriteString("x"); err != nil {
176		t.Fatal(err)
177	}
178	if err := bw.Flush(); err == nil {
179		t.Fatal("failed physical flush returned nil")
180	}
181	if len(observation.writes) != 2 || observation.flushes != 1 {
182		t.Fatalf("failed output was observed: writes=%d flushes=%d", len(observation.writes), observation.flushes)
183	}
184}

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 12

Tags 11

↑↓ moveenter openesc close