|
7 | 7 | "net" |
8 | 8 | "net/http" |
9 | 9 | "net/http/httptest" |
| 10 | + "strings" |
10 | 11 | "testing" |
11 | 12 | "time" |
12 | 13 |
|
@@ -194,6 +195,71 @@ func TestWriteLoop_ConcatenatesMessagesClosedChannel(t *testing.T) { |
194 | 195 | waitDone(t, writeDoneCh, "WriteLoop after closed outbound") |
195 | 196 | } |
196 | 197 |
|
| 198 | +func TestWriteLoop_SplitsAtBatchLimit(t *testing.T) { |
| 199 | + // Drive the outbound backlog well past writeBatchLimit so writeData must split |
| 200 | + // it across more than one frame. The channel is buffered, pre-filled and closed, |
| 201 | + // so the coalescing loop is deterministic: it stops only on reaching the batch |
| 202 | + // limit or draining the (closed) channel, never on a transient empty read. |
| 203 | + msg := WsMsg{Jid: jid.Jid(1234), What: what.Inner, Data: strings.Repeat("x", 4096)} |
| 204 | + frame := msg.Format() |
| 205 | + count := (2*writeBatchLimit)/len(frame) + 2 |
| 206 | + |
| 207 | + outCh := make(chan WsMsg, count) |
| 208 | + for i := 0; i < count; i++ { |
| 209 | + outCh <- msg |
| 210 | + } |
| 211 | + close(outCh) |
| 212 | + |
| 213 | + jawsDoneCh := make(chan struct{}) |
| 214 | + client, server := pipe(t) |
| 215 | + defer func() { _ = client.CloseNow() }() |
| 216 | + defer func() { _ = server.CloseNow() }() |
| 217 | + // Frames at the batch limit exceed coder/websocket's default 32 KB per-message |
| 218 | + // read limit; in production the browser (not a Go reader) consumes them, so |
| 219 | + // lift the limit here to read the server's large outbound frames. |
| 220 | + client.SetReadLimit(-1) |
| 221 | + |
| 222 | + ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) |
| 223 | + defer cancel() |
| 224 | + |
| 225 | + writeDoneCh := make(chan struct{}) |
| 226 | + go func() { |
| 227 | + defer close(writeDoneCh) |
| 228 | + WriteLoop(ctx, nil, jawsDoneCh, outCh, server) |
| 229 | + }() |
| 230 | + |
| 231 | + var frames [][]byte |
| 232 | + var total []byte |
| 233 | + for { |
| 234 | + mt, b, err := client.Read(ctx) |
| 235 | + if err != nil { |
| 236 | + break |
| 237 | + } |
| 238 | + if mt != websocket.MessageText { |
| 239 | + t.Fatalf("frame type = %v, want MessageText", mt) |
| 240 | + } |
| 241 | + frames = append(frames, append([]byte(nil), b...)) |
| 242 | + total = append(total, b...) |
| 243 | + } |
| 244 | + |
| 245 | + waitDone(t, writeDoneCh, "WriteLoop after backlog drained") |
| 246 | + |
| 247 | + if len(frames) < 2 { |
| 248 | + t.Fatalf("got %d frame(s), want the backlog split across more than one", len(frames)) |
| 249 | + } |
| 250 | + // Nothing is dropped, duplicated or reordered: the concatenation of every frame |
| 251 | + // equals the concatenation of every queued message. |
| 252 | + if want := strings.Repeat(frame, count); string(total) != want { |
| 253 | + t.Fatalf("reassembled %d bytes across %d frames, want %d bytes", len(total), len(frames), len(want)) |
| 254 | + } |
| 255 | + // Every frame except the last coalesces up to the batch limit before flushing. |
| 256 | + for i, f := range frames[:len(frames)-1] { |
| 257 | + if len(f) < writeBatchLimit { |
| 258 | + t.Fatalf("frame %d is %d bytes, want >= writeBatchLimit (%d)", i, len(f), writeBatchLimit) |
| 259 | + } |
| 260 | + } |
| 261 | +} |
| 262 | + |
197 | 263 | func TestWriteLoop_RespectsContext(t *testing.T) { |
198 | 264 | outCh := make(chan WsMsg) |
199 | 265 | jawsDoneCh := make(chan struct{}) |
|
0 commit comments