mirror of
https://github.com/soypat/lneto.git
synced 2026-08-31 11:59:05 +00:00
c879d0497c
Ring.onReadEnd and Ring.ReadDiscard call Reset() when the last readable
byte is consumed, which rewinds Off and End to 0. That is a valid empty
state, but it also moves the write position, and bytes staged past that
position with PeekWrite are addressed relative to it. After the rewind a
Commit hands back whatever bytes happen to live at the start of the
buffer instead of the staged ones.
tcp stages out-of-order segments exactly this way (see reassembly.store,
"the ring write pointer advances in lockstep with rcv.NXT, so the staged
bytes are always where seq implies"). Reading is driven by the
application, so a receiver that drains its stream while a gap is open
loses the staged tail silently: the stream arrives with the correct
length and the wrong contents.
Observed on a Sophgo SG2002 (100Mbit DWMAC, receive ring dropping frames
under load): a 16 MiB download arrived complete with a different
SHA-256, and TLS over the same path failed with "bad record MAC", while
the transmit path was bit-perfect.
Mark the ring empty without rewinding instead: End=0 is what empty
means, and the write position is then Off, which Ring.Write already
supports explicitly.
Two tests, both failing before the change:
- internal: staged bytes survive the ring being read empty.
- tcp: a reassembled stream is byte-identical when segments arrive
reordered (segment K arrived as the contents of an earlier segment).
Co-authored-by: Derek den Haas <i.pestano@easyflor.nl>
701 lines
19 KiB
Go
701 lines
19 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")
|
|
}
|
|
}
|
|
|
|
// TestRingPeekWriteSurvivesEmptyRead checks bytes staged with [Ring.PeekWrite]
|
|
// survive the ring being read empty, an event the stager does not control.
|
|
func TestRingPeekWriteSurvivesEmptyRead(t *testing.T) {
|
|
r := &Ring{Buf: make([]byte, 16)}
|
|
if _, err := r.Write([]byte("AAAA")); err != nil {
|
|
t.Fatal("first write:", err)
|
|
}
|
|
// Stage "CCCC" one 4-byte gap past the write position.
|
|
if !r.PeekWrite([]byte("CCCC"), 4) {
|
|
t.Fatal("PeekWrite should fit")
|
|
}
|
|
// Drain everything readable: ring goes empty, staged bytes still pending.
|
|
got := make([]byte, 16)
|
|
n, err := r.Read(got)
|
|
if err != nil || string(got[:n]) != "AAAA" {
|
|
t.Fatalf("drain read %q (%v), want AAAA", got[:n], err)
|
|
}
|
|
if !r.IsEmpty() {
|
|
t.Fatal("ring should be empty after draining")
|
|
}
|
|
// Fill the gap and commit the staged tail.
|
|
if _, err := r.Write([]byte("BBBB")); err != nil {
|
|
t.Fatal("gap write:", err)
|
|
}
|
|
if err := r.Commit(4); err != nil {
|
|
t.Fatal("commit:", err)
|
|
}
|
|
n, err = r.Read(got)
|
|
if err != nil {
|
|
t.Fatal("read:", err)
|
|
}
|
|
if string(got[:n]) != "BBBBCCCC" {
|
|
t.Fatalf("read %q, want BBBBCCCC: the staged bytes were committed from the wrong offset", got[:n])
|
|
}
|
|
testRingSanity(t, r)
|
|
}
|