blob: 06cd3e3b7ab77f04a4915070356ce7825ca21219 [file] [log] [blame]
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package strings_test
import (
"bytes"
"internal/asan"
. "strings"
"testing"
"unicode/utf8"
)
func check(t *testing.T, b *Builder, want string) {
t.Helper()
got := b.String()
if got != want {
t.Errorf("String: got %#q; want %#q", got, want)
return
}
if n := b.Len(); n != len(got) {
t.Errorf("Len: got %d; but len(String()) is %d", n, len(got))
}
if n := b.Cap(); n < len(got) {
t.Errorf("Cap: got %d; but len(String()) is %d", n, len(got))
}
}
func TestBuilder(t *testing.T) {
var b Builder
check(t, &b, "")
n, err := b.WriteString("hello")
if err != nil || n != 5 {
t.Errorf("WriteString: got %d,%s; want 5,nil", n, err)
}
check(t, &b, "hello")
if err = b.WriteByte(' '); err != nil {
t.Errorf("WriteByte: %s", err)
}
check(t, &b, "hello ")
n, err = b.WriteString("world")
if err != nil || n != 5 {
t.Errorf("WriteString: got %d,%s; want 5,nil", n, err)
}
check(t, &b, "hello world")
}
func TestBuilderString(t *testing.T) {
var b Builder
b.WriteString("alpha")
check(t, &b, "alpha")
s1 := b.String()
b.WriteString("beta")
check(t, &b, "alphabeta")
s2 := b.String()
b.WriteString("gamma")
check(t, &b, "alphabetagamma")
s3 := b.String()
// Check that subsequent operations didn't change the returned strings.
if want := "alpha"; s1 != want {
t.Errorf("first String result is now %q; want %q", s1, want)
}
if want := "alphabeta"; s2 != want {
t.Errorf("second String result is now %q; want %q", s2, want)
}
if want := "alphabetagamma"; s3 != want {
t.Errorf("third String result is now %q; want %q", s3, want)
}
}
func TestBuilderReset(t *testing.T) {
var b Builder
check(t, &b, "")
b.WriteString("aaa")
s := b.String()
check(t, &b, "aaa")
b.Reset()
check(t, &b, "")
// Ensure that writing after Reset doesn't alter
// previously returned strings.
b.WriteString("bbb")
check(t, &b, "bbb")
if want := "aaa"; s != want {
t.Errorf("previous String result changed after Reset: got %q; want %q", s, want)
}
}
func TestBuilderGrow(t *testing.T) {
for _, growLen := range []int{0, 100, 1000, 10000, 100000} {
if asan.Enabled {
t.Logf("skipping allocs check for growLen %d: extra allocs with -asan; see #70079", growLen)
continue
}
p := bytes.Repeat([]byte{'a'}, growLen)
allocs := testing.AllocsPerRun(100, func() {
var b Builder
b.Grow(growLen) // should be only alloc, when growLen > 0
if b.Cap() < growLen {
t.Fatalf("growLen=%d: Cap() is lower than growLen", growLen)
}
b.Write(p)
if b.String() != string(p) {
t.Fatalf("growLen=%d: bad data written after Grow", growLen)
}
})
wantAllocs := 1
if growLen == 0 {
wantAllocs = 0
}
if g, w := int(allocs), wantAllocs; g != w {
t.Errorf("growLen=%d: got %d allocs during Write; want %v", growLen, g, w)
}
}
// when growLen < 0, should panic
var a Builder
n := -1
defer func() {
if r := recover(); r == nil {
t.Errorf("a.Grow(%d) should panic()", n)
}
}()
a.Grow(n)
}
func TestBuilderWrite2(t *testing.T) {
const s0 = "hello 世界"
for _, tt := range []struct {
name string
fn func(b *Builder) (int, error)
n int
want string
}{
{
"Write",
func(b *Builder) (int, error) { return b.Write([]byte(s0)) },
len(s0),
s0,
},
{
"WriteRune",
func(b *Builder) (int, error) { return b.WriteRune('a') },
1,
"a",
},
{
"WriteRuneWide",
func(b *Builder) (int, error) { return b.WriteRune('世') },
3,
"世",
},
{
"WriteString",
func(b *Builder) (int, error) { return b.WriteString(s0) },
len(s0),
s0,
},
} {
t.Run(tt.name, func(t *testing.T) {
var b Builder
n, err := tt.fn(&b)
if err != nil {
t.Fatalf("first call: got %s", err)
}
if n != tt.n {
t.Errorf("first call: got n=%d; want %d", n, tt.n)
}
check(t, &b, tt.want)
n, err = tt.fn(&b)
if err != nil {
t.Fatalf("second call: got %s", err)
}
if n != tt.n {
t.Errorf("second call: got n=%d; want %d", n, tt.n)
}
check(t, &b, tt.want+tt.want)
})
}
}
func TestBuilderWriteByte(t *testing.T) {
var b Builder
if err := b.WriteByte('a'); err != nil {
t.Error(err)
}
if err := b.WriteByte(0); err != nil {
t.Error(err)
}
check(t, &b, "a\x00")
}
func TestBuilderAllocs(t *testing.T) {
if asan.Enabled {
t.Skip("test allocates more with -asan; see #70079")
}
// Issue 23382; verify that copyCheck doesn't force the
// Builder to escape and be heap allocated.
n := testing.AllocsPerRun(10000, func() {
var b Builder
b.Grow(5)
b.WriteString("abcde")
_ = b.String()
})
if n != 1 {
t.Errorf("Builder allocs = %v; want 1", n)
}
}
func TestBuilderCopyPanic(t *testing.T) {
tests := []struct {
name string
fn func()
wantPanic bool
}{
{
name: "String",
wantPanic: false,
fn: func() {
var a Builder
a.WriteByte('x')
b := a
_ = b.String() // appease vet
},
},
{
name: "Len",
wantPanic: false,
fn: func() {
var a Builder
a.WriteByte('x')
b := a
b.Len()
},
},
{
name: "Cap",
wantPanic: false,
fn: func() {
var a Builder
a.WriteByte('x')
b := a
b.Cap()
},
},
{
name: "Reset",
wantPanic: false,
fn: func() {
var a Builder
a.WriteByte('x')
b := a
b.Reset()
b.WriteByte('y')
},
},
{
name: "Write",
wantPanic: true,
fn: func() {
var a Builder
a.Write([]byte("x"))
b := a
b.Write([]byte("y"))
},
},
{
name: "WriteByte",
wantPanic: true,
fn: func() {
var a Builder
a.WriteByte('x')
b := a
b.WriteByte('y')
},
},
{
name: "WriteString",
wantPanic: true,
fn: func() {
var a Builder
a.WriteString("x")
b := a
b.WriteString("y")
},
},
{
name: "WriteRune",
wantPanic: true,
fn: func() {
var a Builder
a.WriteRune('x')
b := a
b.WriteRune('y')
},
},
{
name: "Grow",
wantPanic: true,
fn: func() {
var a Builder
a.Grow(1)
b := a
b.Grow(2)
},
},
}
for _, tt := range tests {
didPanic := make(chan bool)
go func() {
defer func() { didPanic <- recover() != nil }()
tt.fn()
}()
if got := <-didPanic; got != tt.wantPanic {
t.Errorf("%s: panicked = %v; want %v", tt.name, got, tt.wantPanic)
}
}
}
func TestBuilderWriteInvalidRune(t *testing.T) {
// Invalid runes, including negative ones, should be written as
// utf8.RuneError.
for _, r := range []rune{-1, utf8.MaxRune + 1} {
var b Builder
b.WriteRune(r)
check(t, &b, "\uFFFD")
}
}
var someBytes = []byte("some bytes sdljlk jsklj3lkjlk djlkjw")
var sinkS string
func benchmarkBuilder(b *testing.B, f func(b *testing.B, numWrite int, grow bool)) {
b.Run("1Write_NoGrow", func(b *testing.B) {
b.ReportAllocs()
f(b, 1, false)
})
b.Run("3Write_NoGrow", func(b *testing.B) {
b.ReportAllocs()
f(b, 3, false)
})
b.Run("3Write_Grow", func(b *testing.B) {
b.ReportAllocs()
f(b, 3, true)
})
}
func BenchmarkBuildString_Builder(b *testing.B) {
benchmarkBuilder(b, func(b *testing.B, numWrite int, grow bool) {
for i := 0; i < b.N; i++ {
var buf Builder
if grow {
buf.Grow(len(someBytes) * numWrite)
}
for i := 0; i < numWrite; i++ {
buf.Write(someBytes)
}
sinkS = buf.String()
}
})
}
func BenchmarkBuildString_WriteString(b *testing.B) {
someString := string(someBytes)
benchmarkBuilder(b, func(b *testing.B, numWrite int, grow bool) {
for i := 0; i < b.N; i++ {
var buf Builder
if grow {
buf.Grow(len(someString) * numWrite)
}
for i := 0; i < numWrite; i++ {
buf.WriteString(someString)
}
sinkS = buf.String()
}
})
}
func BenchmarkBuildString_ByteBuffer(b *testing.B) {
benchmarkBuilder(b, func(b *testing.B, numWrite int, grow bool) {
for i := 0; i < b.N; i++ {
var buf bytes.Buffer
if grow {
buf.Grow(len(someBytes) * numWrite)
}
for i := 0; i < numWrite; i++ {
buf.Write(someBytes)
}
sinkS = buf.String()
}
})
}
func TestBuilderGrowSizeclasses(t *testing.T) {
if asan.Enabled {
t.Skip("test allocates more with -asan; see #70079")
}
s := Repeat("a", 19)
allocs := testing.AllocsPerRun(100, func() {
var b Builder
b.Grow(18)
b.WriteString(s)
_ = b.String()
})
if allocs > 1 {
t.Fatalf("unexpected amount of allocations: %v, want: 1", allocs)
}
}