Files
lneto/internal/ring_test.go
T
Marvin Drees ab1a0c735a Add out-of-order segment reassembly (#148)
* feat(tcp): add out-of-order segment reassembly

Add an opt-in, bounded out-of-order reassembly buffer so a single lost
segment can be recovered by retransmitting the gap while later segments
are held and delivered once the gap fills.

The receiver also subtracts buffered out-of-order bytes from the
advertised receive window and avoids challenge-ACK aborts for in-window
future data. Reassembly is disabled by default.

Generated with LLM assistance.

Signed-off-by: Marvin Drees <marvin.drees@9elements.com>

* implement review feedback around rx buffer reuse

Signed-off-by: Marvin Drees <marvin.drees@9elements.com>

---------

Signed-off-by: Marvin Drees <marvin.drees@9elements.com>
2026-07-10 10:36:15 -03:00

664 lines
18 KiB
Go

package internal
import (
"bytes"
"fmt"
"io"
"math/rand"
"testing"
)
func TestRing(t *testing.T) {
rng := rand.New(rand.NewSource(0))
const overdata = "hello world"
const bufSize = 8
var n int
var err error
var buf [bufSize]byte
r := &Ring{
Buf: make([]byte, bufSize),
}
const data = "hello"
// Set random data and write some more and read it back.
for i := range 32 {
nfirst := max(1, rng.Intn(bufSize)/2)
nsecond := max(1, rng.Intn(bufSize)/2)
if nfirst+nsecond > bufSize {
nfirst = bufSize - nsecond
}
offset := rng.Intn(bufSize - 1)
copy(buf[:], overdata[:nfirst])
setRingData(t, r, offset, buf[:nfirst])
// println("test", r.end, r.off, offset, r)
ngot, err := r.WriteString(overdata[nfirst : nfirst+nsecond])
if err != nil {
t.Fatal(err)
}
if ngot != nsecond {
t.Errorf("%d did not write data correctly: got %d; want %d", i, ngot, nsecond)
}
testRingSanity(t, r)
buf = [bufSize]byte{}
// Case where data wraps around end of buffer.
n, err = r.Read(buf[:])
if err != nil {
break
}
if n != nfirst+nsecond {
t.Errorf("got %d; want %d (%d+%d)", n, nfirst+nsecond, nfirst, nsecond)
}
if string(buf[:n]) != overdata[:n] {
t.Errorf("got %q; want %q", buf[:n], overdata[:n])
}
testRingSanity(t, r)
}
var readback [bufSize]byte
var zeros [bufSize]byte
// Set random data and write some more and read it back with ReadAt and ReadPeek and ReadDiscard.
for range 32 {
nfirst := rng.Intn(len(data))/2 + 1 // write garbage data first.
nsecond := rng.Intn(len(data))/2 + 1
if nfirst+nsecond > bufSize {
nfirst = bufSize - nsecond
}
r.Reset()
randOff := rng.Intn(bufSize)
content := append([]byte{}, zeros[:nfirst]...)
content = append(content, data[:nsecond]...)
setRingData(t, r, randOff, content)
// Two-tap ReadPeek to make sure pointer not advanced.
for range 2 {
n, err = r.ReadPeek(readback[:])
if err != nil && err != io.EOF {
t.Fatal("read failed", err)
} else if n != nfirst+nsecond {
t.Errorf("want!=got bytes read %d, %d", nfirst+nsecond, n)
} else if !bytes.Equal(readback[:nfirst], zeros[:nfirst]) {
t.Error("first section not match")
} else if !bytes.Equal(readback[nfirst:nfirst+nsecond], []byte(data[:nsecond])) {
t.Error("second section not match")
}
testRingSanity(t, r)
}
// Two-tap ReadAt to make sure pointer not advanced.
for range 2 {
off := rng.Intn(nfirst + nsecond)
n, err = r.ReadAt(readback[:nfirst+nsecond-off], int64(off))
readat := readback[:n]
first := zeros[min(off, nfirst):nfirst]
secondOff := max(0, off-nfirst)
second := []byte(data[secondOff:nsecond])
gotSecond := readat[len(first):]
if err != nil && err != io.EOF {
t.Fatal("read failed", off, err)
} else if n != nfirst+nsecond-off {
t.Errorf("want!=got bytes read %d, %d", nfirst+nsecond-off, n)
} else if len(first) > 0 && !bytes.Equal(readat[:nfirst-off], first) {
t.Error("first section not match")
} else if len(second) > 0 && !bytes.Equal(gotSecond, second) {
t.Errorf("second section not match got=%q want=%q", gotSecond, second)
}
testRingSanity(t, r)
}
// ReadDiscard test.
discard := rng.Intn(nfirst+nsecond) + 1
r.ReadDiscard(discard)
n, err := r.Read(readback[:])
if err != nil && err != io.EOF {
t.Fatal(err)
}
wantN := nfirst + nsecond - discard
if wantN != n {
t.Errorf("Want %d bytes read, got %d", wantN, n)
}
if !bytes.Equal(readback[:n], content[discard:]) {
t.Errorf("want data read %q, got %q", content[discard:], readback[:n])
}
testRingSanity(t, r)
}
_ = r._string(0)
}
func TestRing2(t *testing.T) {
const maxsize = 8
const ntests = 80000
rng := rand.New(rand.NewSource(0))
data := make([]byte, maxsize)
ringbuf := make([]byte, maxsize)
auxbuf := make([]byte, maxsize)
rng.Read(data)
for i := range ntests {
dsize := max(rng.Intn(len(data)), 1)
if !testRing1_loopback(t, rng, ringbuf, data[:dsize], auxbuf) {
t.Fatalf("failed test %d", i)
}
}
}
func TestRingEmpty(t *testing.T) {
const bufSize = 8
data := make([]byte, bufSize)
r := &Ring{Buf: data}
readCalls := []func([]byte) (int, error){
r.read,
r.Read,
r.ReadPeek,
}
for _, isResetCalled := range []bool{false, true} {
for _, isonReadEndCalled := range []bool{false, true} {
name := fmt.Sprintf("reset=%v readend=%v", isResetCalled, isonReadEndCalled)
t.Run(name, func(t *testing.T) {
for off := range bufSize + 1 {
r.End = 0
r.Off = off
if isResetCalled {
testRingSanity(t, r)
r.Reset()
}
buf := r.Buffered()
if buf != 0 {
t.Fatalf("want 0 bytes buffered, got %d for off=%d, end=%d size=%d", buf, r.Off, r.End, r.Size())
}
if isonReadEndCalled {
testRingSanity(t, r)
canonRing(r)
}
buf2 := r.Buffered()
if buf2 != 0 {
t.Fatalf("want 0 bytes buffered(second call), got buf=%d->%d for off=%d->%d, end=0->%d size=%d", buf, buf2, off, r.Off, r.End, r.Size())
}
testRingSanity(t, r)
for _, read := range readCalls {
n, err := read(data)
if err != io.EOF {
t.Fatal("want EOF for empty read call")
} else if n != 0 {
t.Fatalf("expected no bytes read, got %d", n)
}
testRingSanity(t, r)
}
}
})
}
}
}
func TestRingNonEmpty(t *testing.T) {
const bufSize = 8
data := make([]byte, bufSize)
r := &Ring{Buf: data}
for _, checkRead := range []bool{false, true} {
for _, checkWrite := range []bool{false, true} {
for _, isonReadEndCalled := range []bool{false, true} {
name := fmt.Sprintf("readend=%v checkWrite=%v checkRead=%v", isonReadEndCalled, checkWrite, checkRead)
t.Run(name, func(t *testing.T) {
for end := 1; end < bufSize+1; end++ {
for off := range bufSize + 1 {
r.End = end
r.Off = off
buf := r.Buffered()
if buf == 0 {
t.Fatalf("want !=0 bytes buffered, got %d for off=%d, end=%d size=%d", buf, r.Off, r.End, r.Size())
}
if isonReadEndCalled {
testRingSanity(t, r)
canonRing(r)
}
buf2 := r.Buffered()
if buf2 != buf {
t.Fatalf("Buffered changed on no-op %d->%d? for off=%d->%d, end=%d->%d size=%d", buf, buf2, off, r.Off, end, r.End, r.Size())
}
if r.Off != off {
t.Fatalf("want off=%d, got off=%d off modified with no read call?!", off, r.Off)
}
if checkWrite {
testRingSanity(t, r)
free := r.Size() - buf
if free != 0 {
n, err := r.Write(data[:free])
if n != free || err != nil {
t.Errorf("want %d to fill buffer, got n=%d err=%v", free, n, err)
}
}
}
if checkRead {
testRingSanity(t, r)
buf := r.Buffered()
n, err := r.Read(data[:buf])
if n != buf || err != nil {
t.Errorf("want %d read bytes, got n=%d err=%v", buf, n, err)
}
}
testRingSanity(t, r)
}
}
})
}
}
}
}
func TestRing_OffWrite(t *testing.T) {
const bufSize = 8
var rawbuf, auxbuf, readback [bufSize]byte
r := &Ring{Buf: rawbuf[:]}
for n := 1; n < bufSize+1; n++ {
for off := range bufSize + 1 {
r.Off = off // Start write at off.
r.End = 0 // Reset to use no data.
for i := 0; i < n; i++ {
rawbuf[(off+i)%len(rawbuf)] = 0
auxbuf[i] = byte(i) + 1
}
ngot, err := r.Write(auxbuf[:n])
if err != nil {
t.Fatal(err)
} else if ngot != n {
t.Fatal(n, ngot)
}
for i := 0; i < n; i++ {
offz := (off + i) % len(rawbuf)
if rawbuf[offz] != auxbuf[i] {
t.Fatalf("mismatch pos=%d off=%d %q!=%q", i, offz, rawbuf[offz], auxbuf[i])
}
}
ngot, err = r.Read(readback[:])
if err != nil {
t.Fatal(err)
} else if ngot != n {
t.Fatal(n, ngot)
} else if !bytes.Equal(readback[:n], auxbuf[:n]) {
t.Fatalf("want readback %q, got %q", auxbuf[:n], readback[:n])
}
}
}
}
func TestRing_TwoWrite(t *testing.T) {
const bufSize = 8
rng := rand.New(rand.NewSource(1))
var rawbuf, auxbuf, readback [bufSize]byte
r := &Ring{Buf: rawbuf[:]}
for range 1024 {
n1 := rng.Intn(bufSize-1) + 1 // leave space for one more write
n2 := rng.Intn(bufSize-n1) + 1
off := rng.Intn(bufSize + 1)
if n1+n2 > r.Size() {
panic("invalid test")
}
r.Reset()
rng.Read(auxbuf[:])
setRingData(t, r, off, auxbuf[:n1])
n2got, err := r.Write(auxbuf[n1 : n1+n2])
if err != nil || n2got != n2 {
t.Fatal(err, n2, n2got)
}
testRingSanity(t, r)
n, err := r.Read(readback[:])
if err != nil {
t.Fatal(err)
} else if n != n1+n2 {
t.Fatalf("failed to read complete written data %d/%d (%d+%d)", n, n1+n2, n1, n2)
} else if !bytes.Equal(readback[:n], auxbuf[:n]) {
t.Fatalf("integrity of data compromised %q!=%q", readback[:n], auxbuf[:n])
}
testRingSanity(t, r)
}
}
func TestRingOverwrite(t *testing.T) {
const bufSize = 8
var rawbuf, auxbuf [bufSize]byte
r := &Ring{Buf: rawbuf[:]}
for off := range bufSize + 1 {
for buf := range bufSize + 1 {
setRingData(t, r, off, rawbuf[:buf])
// Select write size overwriting data.
for osz := bufSize - buf + 1; osz < bufSize+1; osz++ {
free := r.Free()
if osz <= free {
panic("invalid test")
}
ngot, err := r.Write(auxbuf[:osz])
if err == nil {
t.Fatal("expected error")
} else if ngot > 0 {
t.Fatalf("expected no data written, got %d", ngot)
}
}
}
}
}
// func TestRingWriteLimited(t *testing.T) {
// rng := rand.New(rand.NewSource(2))
// const bufSize = 8
// r := &Ring{
// Buf: make([]byte, bufSize),
// }
// testRingSanity(t, r)
// var data [bufSize]byte
// var wdata [bufSize]byte
// for i := 0; i < 10000; i++ {
// for i := range r.Buf {
// r.Buf[i] = 0
// }
// buffered, _ := rng.Read(data[:rng.Intn(bufSize-2)+1])
// off := rng.Intn(bufSize)
// setRingData(t, r, off, data[:buffered])
// if r.Buffered() != buffered {
// t.Fatalf("failed to set buffered amount of data")
// } else if r.Off != off {
// t.Fatal("bad offset")
// }
// limOff := rng.Intn(bufSize) + 1
// freeLim := r.FreeLimited(limOff)
// free := r.Free()
// toWrite := rng.Intn(freeLim+1) + 1
// rng.Read(wdata[:toWrite])
// var wantN int
// isContiguous := limOff > r.End
// if isContiguous {
// wantN = min(toWrite, limOff-r.End)
// } else {
// wantN = min(toWrite, len(r.Buf)-r.End+limOff)
// }
// overwrite := toWrite > wantN
// n, err := r.WriteLimited(wdata[:toWrite], limOff)
// if !overwrite && err != nil {
// t.Errorf("limited write: %s", err)
// t.Errorf("size=%d off=%d end=%d lim=%d nwrite=%d", r.Size(), r.Off, r.End, limOff, toWrite)
// } else if !overwrite && n != wantN {
// t.Errorf("nwant=%d ngot=%d off=%d lim=%d towrite=%d buffered=%d/%d wantremain=%d gotremain=%d endOff=%d", wantN, n, off, limOff, toWrite, buffered, r.Size(), free-wantN, free-n, r.Off)
// } else if overwrite && (err == nil || n != 0) {
// t.Errorf("expected full buffer error and no data written on limit overwrite, got %d", n)
// }
// for i := r.End % r.Size(); i != limOff && i != r.Off; i = (i + 1) % r.Size() {
// if r.Buf[i] != 0 {
// t.Fatalf("OVERWRITE pos=%d end=%d lim=%d", i, r.End, limOff)
// }
// }
// testRingSanity(t, r)
// }
// }
func TestRing_findcrash(t *testing.T) {
const maxsize = 33
const ntests = 800000
r := Ring{
Buf: make([]byte, maxsize*6),
}
rng := rand.New(rand.NewSource(0))
data := make([]byte, maxsize)
for i := range ntests {
free := r.Free()
if free < 0 {
t.Fatal("free < 0")
}
if rng.Intn(2) == 0 {
l := max(rng.Intn(len(data)), 1)
if l > free {
continue // Buffer full.
}
n, err := r.Write(data[:l])
expectFree := free - n
free = r.Free()
if n != l {
t.Fatal(i, "write failed", n, l, err)
} else if expectFree != free {
t.Fatal(i, "free not updated correctly", expectFree, free)
}
testRingSanity(t, &r)
}
buffered := r.Buffered()
if buffered < 0 {
t.Fatal("buffered < 0")
}
if rng.Intn(2) == 0 {
l := max(rng.Intn(len(data)), 1)
n, err := r.Read(data[:l])
expectRead := min(buffered, l)
expectBuffered := buffered - n
buffered = r.Buffered()
if n != expectRead {
t.Fatal(i, "read failed", n, l, expectRead, err)
} else if buffered != expectBuffered {
t.Fatal(i, "buffered not updated correctly", expectBuffered, buffered)
}
testRingSanity(t, &r)
}
}
}
func TestRingFreeLimited(t *testing.T) {
const n = 16
rng := rand.New(rand.NewSource(1))
buffer := make([]byte, n)
r := Ring{Buf: buffer}
for itest := range 100000 {
r.Off = rng.Intn(n)
r.End = rng.Intn(n + 1)
limit := rng.Intn(n)
wantFreeLimited := 0
writeStart := r.End
if writeStart == 0 {
// Empty buffer case.
writeStart = r.Off
for i := writeStart % n; i != limit; i = (i + 1) % n {
wantFreeLimited++
}
} else {
// Buffer with contents. Beware limit and own offset.
for i := writeStart % n; i != limit && i != r.Off; i = (i + 1) % n {
wantFreeLimited++
}
}
gotFreeLimited := r.FreeLimited(limit)
if gotFreeLimited != wantFreeLimited {
t.Fatalf("[%d] len=%d off=%d end=%d limit=%d GOT=%d, WANT=%d", itest, n, r.Off, r.End, limit, gotFreeLimited, wantFreeLimited)
}
}
}
func testRing1_loopback(t *testing.T, rng *rand.Rand, ringbuf, data, auxbuf []byte) bool {
if len(data) > len(ringbuf) || len(data) > len(auxbuf) {
panic("invalid ringbuf or data")
}
dsize := len(data)
var r Ring
r.Buf = ringbuf
nfirst := rng.Intn(dsize) / 2
nsecond := rng.Intn(dsize) / 2
if nfirst == 0 || nsecond == 0 {
return true
}
offset := rng.Intn(dsize - 1)
setRingData(t, &r, offset, data[:nfirst])
ngot, err := r.Write(data[nfirst : nfirst+nsecond])
if err != nil {
t.Error(err)
return false
}
if ngot != nsecond {
t.Errorf("did not write data correctly: got %d; want %d", ngot, nsecond)
}
testRingSanity(t, &r)
// Case where data wraps around end of buffer.
n, err := r.Read(auxbuf[:])
if err != nil {
t.Error(err)
return false
}
testRingSanity(t, &r)
if n != nfirst+nsecond {
t.Errorf("got %d; want %d (%d+%d)", n, nfirst+nsecond, nfirst, nsecond)
}
if !bytes.Equal(auxbuf[:n], data[:n]) {
t.Errorf("got %q; want %q", auxbuf[:n], data[:n])
}
return !t.Failed()
}
func setRingData(t *testing.T, r *Ring, offset int, data []byte) {
t.Helper()
sz := r.Size()
if len(data) > sz {
panic("data too large")
}
n := copy(r.Buf[offset:], data)
if len(data) > 0 {
r.End = offset + n
if len(data)+offset > sz {
// End of buffer not enough to hold data, wrap around.
n = copy(r.Buf, data[n:])
r.End = n
}
} else {
r.End = 0
}
r.Off = offset
canonRing(r)
// println("buf:", sz, "end:", r.end, "off:", r.off, offset, "data:", len(data))
free := r.Free()
wantFree := sz - len(data)
if free != wantFree {
t.Fatalf("free got %d; want %d", free, wantFree)
}
buffered := r.Buffered()
wantBuffered := len(data)
if buffered != wantBuffered {
t.Fatalf("buffered got %d; want %d", buffered, wantBuffered)
}
end := r.End
off := r.Off
sdata := r.string()
if sdata != string(data) {
t.Fatalf("data got %q; want %q", sdata, data)
}
r.End = end
r.Off = off
testRingSanity(t, r)
}
func testRingSanity(t *testing.T, r *Ring) {
// t.Helper() // Really costly call. Avoid calling it every single function call.
buf := r.Buffered()
free := r.Free()
sz := r.Size()
if r.End == 0 && buf > 0 {
t.Helper()
t.Fatalf("want end=0 to encode no data, got off=%d end=%d => buffered=%d", r.Off, r.End, r.Buffered())
} else if sz != free+buf {
t.Helper()
t.Fatalf("want size=free+buffered, got %d=%d+%d", sz, free, buf)
} else if r.End != 0 && r.Off == r.End && buf != sz {
t.Helper()
t.Fatalf("want (off==end && end!=0) to encode full buffer, got off=%d end=%d show fill ration %d/%d", r.Off, r.End, buf, sz)
}
}
func canonRing(r *Ring) {
if r.Buffered() == 0 {
r.End = r.addOff(r.Off, 1)
r.onReadEnd(1)
}
}
// TestRingPeekWriteCommit verifies that bytes staged ahead of a gap with
// PeekWrite become readable in order once the gap is filled and committed.
func TestRingPeekWriteCommit(t *testing.T) {
r := &Ring{Buf: make([]byte, 16)}
// Stage "BBBB" 4 bytes ahead of the write position (the gap).
if !r.PeekWrite([]byte("BBBB"), 4) {
t.Fatal("PeekWrite should fit")
}
// Staged bytes are not yet readable.
if r.Buffered() != 0 {
t.Fatalf("staged bytes must not be readable, buffered=%d", r.Buffered())
}
// Fill the gap with a normal write, then commit the staged tail.
if _, err := r.Write([]byte("AAAA")); err != nil {
t.Fatal("gap write:", err)
}
if err := r.Commit(4); err != nil {
t.Fatal("commit:", err)
}
got := make([]byte, 8)
n, err := r.Read(got)
if err != nil {
t.Fatal("read:", err)
}
if string(got[:n]) != "AAAABBBB" {
t.Fatalf("read %q, want AAAABBBB", got[:n])
}
testRingSanity(t, r)
}
// TestRingPeekWriteWrap exercises PeekWrite/Commit when the staged region wraps
// across the end of the backing buffer. Existing data at Off=2,End=6 puts the
// write position at index 6, so a 2-byte gap fills indices 6,7 and the staged
// tail wraps to indices 0,1.
func TestRingPeekWriteWrap(t *testing.T) {
r := &Ring{Buf: make([]byte, 8)}
setRingData(t, r, 2, []byte("WXYZ")) // Off=2, End=6, 4 bytes buffered.
if r.writeStart() != 6 {
t.Fatalf("writeStart=%d, want 6", r.writeStart())
}
// Stage "CD" 2 bytes ahead of the write position (6) → wraps to indices 0,1.
if !r.PeekWrite([]byte("CD"), 2) {
t.Fatal("PeekWrite (wrap) should fit")
}
// Fill the 2-byte gap at indices 6,7, then commit the wrapped tail.
if _, err := r.Write([]byte("AB")); err != nil {
t.Fatal("gap write:", err)
}
if err := r.Commit(2); err != nil {
t.Fatal("commit:", err)
}
got := make([]byte, 8)
n, err := r.Read(got)
if err != nil {
t.Fatal("read:", err)
}
if string(got[:n]) != "WXYZABCD" {
t.Fatalf("read %q, want WXYZABCD", got[:n])
}
testRingSanity(t, r)
}
func TestRingPeekWriteRejects(t *testing.T) {
r := &Ring{Buf: make([]byte, 8)}
if r.PeekWrite([]byte("toolong!!"), 0) {
t.Error("PeekWrite must reject data larger than the buffer")
}
if r.PeekWrite([]byte("data"), 5) { // 5+4 > 8 free.
t.Error("PeekWrite must reject offset+len beyond free space")
}
if r.PeekWrite([]byte("x"), -1) {
t.Error("PeekWrite must reject negative offset")
}
if err := r.Commit(0); err == nil {
t.Error("Commit(0) must error")
}
if err := r.Commit(9); err == nil {
t.Error("Commit beyond free must error")
}
}