Skip to content

Commit cdd58cd

Browse files
fix: add new package truncbuffer
1 parent ab29fd2 commit cdd58cd

2 files changed

Lines changed: 137 additions & 0 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package truncbuffer
2+
3+
import "sync"
4+
5+
// TruncBuffer is a ring buffer that retains only the last max bytes,
6+
// discarding older data. This is useful for capturing stderr output in a
7+
// memory-bounded way when the full output may be arbitrarily large.
8+
// All methods are safe for concurrent use.
9+
type TruncBuffer struct {
10+
mu sync.RWMutex
11+
buf []byte
12+
head int
13+
size int
14+
full bool
15+
}
16+
17+
// NewTruncBuffer creates a TruncBuffer that keeps at most n bytes.
18+
func NewTruncBuffer(n int) *TruncBuffer {
19+
return &TruncBuffer{
20+
buf: make([]byte, n),
21+
size: n,
22+
}
23+
}
24+
25+
// Write appends p to the buffer, keeping only the last size bytes.
26+
// The return value n is the length of p;
27+
func (t *TruncBuffer) Write(p []byte) (int, error) {
28+
t.mu.Lock()
29+
defer t.mu.Unlock()
30+
31+
// If input is larger than the buffer, only keep the tail.
32+
if len(p) >= t.size {
33+
copy(t.buf, p[len(p)-t.size:])
34+
t.head = 0
35+
t.full = true
36+
return len(p), nil
37+
}
38+
39+
for _, b := range p {
40+
t.buf[t.head] = b
41+
t.head++
42+
if t.head == t.size {
43+
t.head = 0
44+
t.full = true
45+
}
46+
}
47+
48+
return len(p), nil
49+
}
50+
51+
// Bytes returns a copy of the current buffer contents in order.
52+
func (t *TruncBuffer) Bytes() []byte {
53+
t.mu.RLock()
54+
defer t.mu.RUnlock()
55+
56+
if !t.full {
57+
return append([]byte(nil), t.buf[:t.head]...)
58+
}
59+
60+
out := make([]byte, t.size)
61+
n := copy(out, t.buf[t.head:])
62+
copy(out[n:], t.buf[:t.head])
63+
return out
64+
}
65+
66+
// String returns the buffer contents as a string.
67+
func (t *TruncBuffer) String() string {
68+
return string(t.Bytes())
69+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package truncbuffer
2+
3+
import (
4+
"io"
5+
"sync"
6+
"testing"
7+
)
8+
9+
var _ io.Writer = (*TruncBuffer)(nil)
10+
11+
func TestTruncBuffer_SmallWrites(t *testing.T) {
12+
tb := NewTruncBuffer(10)
13+
tb.Write([]byte("hello"))
14+
if got := string(tb.Bytes()); got != "hello" {
15+
t.Fatalf("got %q, want %q", got, "hello")
16+
}
17+
}
18+
19+
func TestTruncBuffer_ExactMax(t *testing.T) {
20+
tb := NewTruncBuffer(5)
21+
tb.Write([]byte("abcde"))
22+
if got := string(tb.Bytes()); got != "abcde" {
23+
t.Fatalf("got %q, want %q", got, "abcde")
24+
}
25+
}
26+
27+
func TestTruncBuffer_OverflowSingleWrite(t *testing.T) {
28+
tb := NewTruncBuffer(5)
29+
tb.Write([]byte("abcdefgh"))
30+
if got := string(tb.Bytes()); got != "defgh" {
31+
t.Fatalf("got %q, want %q", got, "defgh")
32+
}
33+
}
34+
35+
func TestTruncBuffer_OverflowMultipleWrites(t *testing.T) {
36+
tb := NewTruncBuffer(6)
37+
tb.Write([]byte("abc"))
38+
tb.Write([]byte("defgh"))
39+
if got := string(tb.Bytes()); got != "cdefgh" {
40+
t.Fatalf("got %q, want %q", got, "cdefgh")
41+
}
42+
}
43+
44+
func TestTruncBuffer_ManySmallWrites(t *testing.T) {
45+
tb := NewTruncBuffer(4)
46+
for _, b := range []byte("abcdefg") {
47+
tb.Write([]byte{b})
48+
}
49+
if got := string(tb.Bytes()); got != "defg" {
50+
t.Fatalf("got %q, want %q", got, "defg")
51+
}
52+
}
53+
54+
func TestTruncBuffer_ConcurrentWrites(t *testing.T) {
55+
tb := NewTruncBuffer(64)
56+
var wg sync.WaitGroup
57+
for i := 0; i < 100; i++ {
58+
wg.Add(1)
59+
go func() {
60+
defer wg.Done()
61+
tb.Write([]byte("abcdefgh"))
62+
}()
63+
}
64+
wg.Wait()
65+
if got := len(tb.Bytes()); got > 64 {
66+
t.Fatalf("buffer exceeded max: got %d bytes", got)
67+
}
68+
}

0 commit comments

Comments
 (0)