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}