113 lines
3.5 KiB
Go
113 lines
3.5 KiB
Go
package client
|
|
|
|
import (
|
|
"bytes"
|
|
"testing"
|
|
)
|
|
|
|
// The retained region is what a reattach replays from, so an off-by-one here is
|
|
// not a dropped byte but a spliced stream: the peer resumes mid-packet and the
|
|
// session dies in a way no round-trip test would attribute to this code.
|
|
|
|
func TestUnackedTracksOffsets(t *testing.T) {
|
|
var u unackedBuf
|
|
u.append([]byte("hello"))
|
|
u.append([]byte("world"))
|
|
|
|
if got := u.length(); got != 10 {
|
|
t.Fatalf("length = %d, want 10", got)
|
|
}
|
|
if got := u.end(); got != 10 {
|
|
t.Fatalf("end = %d, want 10", got)
|
|
}
|
|
if got := u.from(0); !bytes.Equal(got, []byte("helloworld")) {
|
|
t.Fatalf("from(0) = %q", got)
|
|
}
|
|
// A reattach replays from wherever the peer got to, which lands anywhere —
|
|
// including the middle of a chunk boundary.
|
|
if got := u.from(3); !bytes.Equal(got, []byte("loworld")) {
|
|
t.Fatalf("from(3) = %q", got)
|
|
}
|
|
if got := u.from(10); len(got) != 0 {
|
|
t.Fatalf("from(end) = %q, want empty", got)
|
|
}
|
|
}
|
|
|
|
func TestUnackedAdvanceDropsCreditedBytes(t *testing.T) {
|
|
var u unackedBuf
|
|
u.append([]byte("abcdefghij"))
|
|
|
|
u.advance(4)
|
|
if got := u.length(); got != 6 {
|
|
t.Fatalf("length after advance = %d, want 6", got)
|
|
}
|
|
if got := u.end(); got != 10 {
|
|
t.Fatalf("end must not move when bytes are dropped: got %d, want 10", got)
|
|
}
|
|
if got := u.from(4); !bytes.Equal(got, []byte("efghij")) {
|
|
t.Fatalf("from(4) = %q", got)
|
|
}
|
|
// Below the retained region: the peer named an offset we can no longer
|
|
// satisfy, which must be reported rather than silently clamped — replaying
|
|
// the wrong range is worse than refusing to replay.
|
|
if got := u.from(3); got != nil {
|
|
t.Fatalf("from(3) below base = %q, want nil", got)
|
|
}
|
|
if got := u.from(11); got != nil {
|
|
t.Fatalf("from(11) past end = %q, want nil", got)
|
|
}
|
|
}
|
|
|
|
// Interleaving appends and advances is the steady-state pattern: credit arrives
|
|
// every half window while the sender keeps writing. The buffer must stay exact
|
|
// across the compaction that eventually triggers.
|
|
func TestUnackedSurvivesInterleavedAppendAndAdvance(t *testing.T) {
|
|
var u unackedBuf
|
|
var sent []byte
|
|
var acked int64
|
|
|
|
for i := 0; i < 200; i++ {
|
|
chunk := bytes.Repeat([]byte{byte(i)}, 97)
|
|
sent = append(sent, chunk...)
|
|
// emit's order: reclaim what has been credited so far, then retain the
|
|
// new chunk. The base therefore trails the credit that arrived since.
|
|
base := acked
|
|
u.advance(base)
|
|
u.append(chunk)
|
|
|
|
if got, want := u.end(), int64(len(sent)); got != want {
|
|
t.Fatalf("round %d: end = %d, want %d", i, got, want)
|
|
}
|
|
if got, want := u.length(), len(sent)-int(base); got != want {
|
|
t.Fatalf("round %d: length = %d, want %d", i, got, want)
|
|
}
|
|
if got, want := u.from(base), sent[base:]; !bytes.Equal(got, want) {
|
|
t.Fatalf("round %d: retained region diverges from what was sent", i)
|
|
}
|
|
|
|
// The peer can only ever credit bytes it has actually received.
|
|
if i%3 == 0 {
|
|
if acked += 61; acked > int64(len(sent)) {
|
|
acked = int64(len(sent))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Compaction reuses the backing array, so a stream that runs for hours must not
|
|
// grow one: this is a full window per stream, on both sides.
|
|
func TestUnackedReclaimsBackingArray(t *testing.T) {
|
|
var u unackedBuf
|
|
chunk := bytes.Repeat([]byte{7}, 4096)
|
|
for i := 0; i < 500; i++ {
|
|
u.advance(u.end()) // fully credited every round
|
|
u.append(chunk)
|
|
}
|
|
if u.length() != len(chunk) {
|
|
t.Fatalf("length = %d, want %d", u.length(), len(chunk))
|
|
}
|
|
if cap(u.buf) > 8*len(chunk) {
|
|
t.Fatalf("backing array grew to %d bytes for a %d-byte window", cap(u.buf), len(chunk))
|
|
}
|
|
}
|